November 4, 2009 6:53 PM
Google Reader does not have an official API. However, Nial Kennedy and
the project pyrfeed have excellent documentation on it. I had
developed a Google Reader client in python that the
web version of Readefine uses1. I needed to leverage the same AS3
code base and write an AS3 Google Reader client on top of it for the
desktop version. I managed to do it in a day while ensuring a common
code-base for the AIR and Flex versions thanks to
conditional compilation. This article explains Google Reader's API
along with the AS3 implementation and how it merged in with the
web-version.
Interestingly, there is AS3 code for Google Reader access in snackr's code. I wrote my own though because I needed it to seamlessly merge with my existing Flex code. Regardless, this is the screen-shot of how Readefine Desktop looks with Google Reader access and all chrome minimized:
For more screen-shots, visit Readefine Desktop.
Google Reader API
Google Reader is excellently architected. Its view in HTML and Javascript is independent of the actual methods to get and update Google Reader2.
There are three main parts to the API: authentication, getting data and updating data.
Kindly read the articles referenced in the first paragraph of the article before proceeding. The information regarding the API here augments that information.
Authentication
ClientLogin is what you have to use and it is well documented by
Google. You have to do an HTTP POST to a URL while setting the service
as reader
and passing the username and password.
Ideally, an OAuth or AuthSub implementation would help third-party applications to leverage Google Reader without having users enter their precious skynet password into their application. Let us hope Google releases that in the near future.
Getting Data
The flow for a Google Reader Client is as follows (after auth):
1) Get user info by asking /api/0/user-info
.
2) Get subscription list. Each subscription contains a
"category" if the user had created folders. It also has a field called
firstitemmsec
that denotes in milliseconds the time from which entries
for that feed should be picked up.
firstitemmsec
initially stumped me until I added a new subscription. I
noticed that Google Reader has entries for a feed spanning back to a
month (probably -infinity). So the reader has to know to show you
articles only from the time you subscribed to a feed.
ot
is the parameter that takes firstitemmsec / 1000
when you are
fetching the reading list or a particular feed.
3) Get unread counts. The total will be with the feed id
user/userid_obtained_in_step1/state/com.google/reading-list
. This has
an upper bound of 1000, so 1000 is always 1000+.
4) Get the reading list or a particular feed while being careful to
pass in the correct ot
. Each article in the feed will have special
fields if it is read, starred, kept-unread, etc.
5) To get the next set of articles, pass in a "cont" GET parameter to the call in 4.
Every now and then, you have to fetch unread counts because it changes often depending upon the amount of feeds you are subscribed to.
Updating Data
Marking as read, star, like, share, etc. all fall into this category. This is slightly more tricky than getting data because it involves a token.
This is what I understood:
1) Get a token by posting to /api/0/token
. Save the current timestamp.
2) Every API call that requires a token must be queued so that there is only one outstanding call at a time.
3) Before making an update API call, check your saved timestamp to see if it has been more than 20 minutes3. If it has, get a new token, then continue with the API call.
Google Reader Client in AIR
I have a ServiceLocator singleton class with methods like
greaderGet()
, greaderStar()
, etc. In that method, depending upon a
compilation variable, I either call the AIR code or my python server
side code.
public function googleReaderUnreadCount():void { CONFIG::FLEX { var s:HTTPService = getGReaderService();; var token:AsyncToken = s.send({a: 'c'}); token.addResponder(new GReaderCommand(GReaderCommand.GREADER_UNREAD)); } CONFIG::AIR { greaderClient.getUnreadCount(); } }
Since I'm using an AsyncToken, I have a GReaderCommand
, a class that
implements an IResponder
. The command's result()
method expects data
to be an Object
that you normally get when resultFormat is "object" in
an HTTPService
.
GReaderClient is the AIR class that talks to Google Reader using
a URLLoader
. It takes in the parameters object that was passed to
HTTPService's send(). Each method knows its GReaderCommand type.
It then creates a map of requests to loader (for getting the
parameters) and a map of urlloaders to command.
After performing any API call to Google Reader, there is
a single common result handler that marshall's the response based on
how GReaderCommand
expects it.
private function handleReaderResultEvent(event: Event): void { var urlloader:URLLoader = event.target as URLLoader; var req:URLRequest = clearFromQueue(urlloader); var result:String = String(urlloader.data); urlloader.removeEventListener(Event.COMPLETE, handleReaderResultEvent); urlloader.removeEventListener(IOErrorEvent.IO_ERROR, handleReaderFaultEvent); if ( !req ) return; var gCommand:GReaderCommand = clearCommandFromQueue(req); if ( gCommand ) { marshallReaderResponse(gCommand, result); } }
The marshallReaderResponse creates objects that GReaderCommand's result() method expects which may be object or e4x. In the case of object when the response from Reader is XML, I use an XMLDocument along with a SimpleXMLDecoder. If the response from Reader is in JSON, I parse it using JSONDecoder and build the appropriate object.
Creating an object from XML:
xmlDecoder = new SimpleXMLDecoder(true); try { xmlDoc = new XMLDocument(result); } catch(err:Error) { xmlDoc = null; } if ( !xmlDoc ) obj = xmlDecoder.decodeXML(xmlDoc);
The marshall method finally calls the command's result()
method with
the data.
Here is the full source for my GReaderClient.as. It is incomplete without GReaderCommand, but this you should serve you as a good starting point in developing an Adobe AIR Google Reader client.
For more information on Readefine Desktop, visit readefine.anirudhsasikumar.net.
[1] Since cross-domain requests are not allowed from flash player
unless explicitly allowed.
[2] Except for quickadd which seems to return HTML and JSON data in
the end in a script block.
[3] Time interval obtained after careless observation. Might be incorrect.