Advertisement

Building a Mobile Web Application with the Google Reader API

by

This tutorial will teach you how to build a mobile news feed reader web application using the Google Reader API. This application will be very "feature focused", but the foundation you learn here should be enough for you to expand upon in your own apps. As of the time of this writing, there isn't an official Google Reader API, so this tutorial will make use of the unofficial API commonly used when building Google Reader applications.


Step 1: Concepts and Endpoints

Concepts

Google uses slightly different lingo for some of the backend parts of the Google Reader API. Some of it is tied to their platform and some seems to be due to drift as the application matured on the front-end. One such concept is referred to as a "stream". A stream represents content that is sliced and diced or filterd in different ways. All the articles from a specific feed are part of a stream, and so are all the articles from a folder.

The following is a brief table to show differences in common client-side lingo and the corresponding Google Reader API backend terminology:

Client-side API
Folder / Tag Tag / Label
Subscription Subscription
People you Follow Friend
Starred Starred
Shared Broadcast

Endpoints

If you are logged into your Google Reader account, try entering the following endpoints in your browser:

User information:
https://www.google.com/reader/api/0/user-info?output=json

List of subscriptions:
https://www.google.com/reader/api/0/subscription/list?output=json

List of folders/tags:
https://www.google.com/reader/api/0/tag/list?output=json

List of people you follow:
https://www.google.com/reader/api/0/friend/list?output=json

List of Google Reader preferences:
https://www.google.com/reader/api/0/preference/stream/list?output=json

The above endpoints are read-only, and will be used throughout this tutorial.


Step 2: Building the Interface

To access Google Reader we'll build a custom PHP class. Below is a list of the functions this class will need (I'll go into each in more depth later on):

Class Functions

  • login - Gets Google auth code and reader edit token.
  • get_subscriptions - Returns a list of subscribed feeds.
  • get_tags - Returns a list of folders/tags.
  • get_friends - Returns a list of people you follow.
  • get_stream_items - Returns an article from a stream.
  • set_article_read - Sets an article as read.
  • set_article_starred - Stars an article.
  • set_article_broadcast - Adds an article to your shared list.
  • set_article_review - Applies the tag "review" to an article.
  • get_url - HTTP GET request with Google authentication.
  • get_anon_url - HTTP GET request without Google authentication.
  • post_url - HTTP POST request with Google authentication.
  • post_anon_url - HTTP POST request without Google authentication.

Properties

Our PHP class will use a number of different properties.

As you might be able to guess, the following properties will be set to your Google Reader account's email ID and password:

   public $grEmail  = '';
   public $grPasswd = '';

The following properties are used in the class Curl functions (check out the source code for those):

   public $userAgent = 'tuts+rss+bot';
   public $proxy     = 0;
   public $proxyUrl  = '';

I've included proxy support because my hosting ISP (Godaddy) requires outbound traffic to pass through a proxy. If you don't need proxy support keep $proxy set to 0 in gr-config.php.

Next, we have many of the beginning endpoints for the API's available:

   protected $_urlBase          =  'https://www.google.com';
   protected $_urlApi           =  'https://www.google.com/reader/api/0';
   protected $_urlAuth          =  'https://www.google.com/accounts/ClientLogin';
   protected $_urlToken         =  'https://www.google.com/reader/api/0/token';
   protected $_urlUserInfo      =  'https://www.google.com/reader/api/0/user-info';
   protected $_urlTag           =  'https://www.google.com/reader/api/0/tag';
   protected $_urlSubscription  =  'https://www.google.com/reader/api/0/subscription';
   protected $_urlStream        =  'https://www.google.com/reader/api/0/stream';
   protected $_urlFriend        =  'https://www.google.com/reader/api/0/friend';

The next three properties store information collected during the authentication process:

   public $userInfo  = '';
   protected $auth   = '';
   protected $token  = '';

In the application section we'll set these to PHP sessions so we don't have to login again with each request.


Step 3: Fleshing Out the Class Functions

The following functions will do the heavy-lifting for our application.

Login Function

We need to hit the Google Authentication API to start off:

    $data = '&Email='.$this->grEmail.'&Passwd='.$this->grPasswd.'&service=reader&source='.$this->userAgent.'&continue=http://www.google.com';
    $result = $this->post_anon_url($this->_urlAuth,$data);

After querying the authentication API, the first thing to collect is the $auth string. This string supplies Google's authentication layer with our credentials. Getting the $auth is useful for almost any Google service.

    preg_match('/Auth=(\S*)/', $result, $match);
    $this->auth = $match[1];

Next is the $token and we'll need to hit another endpoint for it. Token is specific to Google Reader and is required for requests that write or change state.

    $this->token = $this->get_url($this->_urlToken);

The third is a user information object, we'll need the userId portion. Some of the API options require your google userId, which is a large number that represents your account and not the email ID you use for logins. The user info also contains the large number used to access your shared feed.

    $this->userInfo = json_decode($this->get_url($this->_urlUserInfo));

List Functions

These are simple functions to retrieve a list of your feeds in various ways.

get_subscriptions returns a list of every feed you have subscribed to:

function get_subscriptions() {
    $result = $this->get_url($this->_urlSubscription.'/list?output=json');
    return json_decode($result);
}

get_tags returns a list of all the folders and tags:

function get_tags() {
    $result = $this->get_url($this->_urlTag.'/list?output=json');
    return json_decode($result);
}

get_friends returns a list of the people you follow:

function get_friends() {
    $result = $this->get_url($this->_urlFriend.'/list?output=json');
    return json_decode($result);
}

Items Function

This is the meat of the class and what everything else leads up to. With this function we are returning the individual articles based on the criteria specified. The $stream is pulled from the results of the list functions, and it could be a feed from a friend, an entire folder, or a specific feed. $n is the number of articles to return at one time. For the app i've limited it to just 20, but if your reusing this for anything other then real-time display you can bump it up to 1000. After 1000 you need to use the continuation option, which is out of scope for this article.

get_stream_items accepts a number of parameters and with the defaults you only really need to provide it with a stream:

function get_stream_items(
    $stream = '',
    $xt_a = array('user/-/state/com.google/read'),
    $daysago = 3, 
    $n = 20, 
    $magic = True) {

$ot is used to tell Google Reader you don't want to return any articles older them this time. Google Reader goes by epoch time, but I like using $daysago because it's just easier to go by days rather than seconds. From that, we can calculate $ot from the current time. $ck is important, but I've never received a good explanation about it, and without an official Google Reader API it may remain a mystery. I've been setting it to time() * 1000 and it just works, whereas time() * 100 does not.

    $ot = time() - ($daysago * 86400);
    $ck = time() * 1000;

$magic is a boolean to return the articles using the Google Reader "magic" ranking, or, if false, by the newest first. You can also sort by oldest first by setting $r = 'o' if you want.

      if ($magic == True) { $r = 'a'; } else { $r = 'n'; }

$xt_a is used to exclude articles of a feed based on certain metadata related to the article's state. By default the function only returns unread posts because it's excluding articles flagged as read.

    $xt = '';
    foreach($xt_a as $key=>$value) { $xt .= '&xt='.$value; }

We pass the final URL to Google Reader and return the results after the parameters are set:

    $url = $this->_urlStream.'/contents/'.$stream.'?ot='.$ot.'&r='.$r.$xt.'&n='.$n.'&ck='.$ck.'&client='.$this->userAgent;
    $result = $this->get_url($url);
    return json_decode($result);
}

Set Function

Three of these functions are used to write back to Google Reader and mark articles with a certain state. The last is used to set a tag. You could easily condense all four down to a single function and pass the "a=" as a function parameter. This set up is just my preference, I prefer it this way so the code using the class is cleaner and easier to read. If you plan on creating your own function you can submit multiple "a=" at a time. Also, you can pass multiple "i=" and "s=" options, you just have to pass an equal amount and in the correct order. I generally pass 10 at a time when marking a lot of items as read.

Set the article to the "read" state:

function set_article_read($id,$stream) {
    $url = $this->_urlApi . '/edit-tag?pos=0&client=' . $this->userAgent;
    $data = 'a=user/-/state/com.google/read&async=true&s='.$stream.'&i='.$id.'&T='.$this->token;
    return $this->post_url($url,$data);
}

Star the article:

function set_article_starred($id,$stream) {
    $url = $this->_urlApi . '/edit-tag?pos=0&client=' . $this->userAgent;
    $data = 'a=user/-/state/com.google/starred&async=true&s='.$stream.'&i='.$id.'&T='.$this->token;
    return $this->post_url($url,$data);
}

Share the article with those who follow you:

function set_article_broadcast($id,$stream) {
    $url = $this->_urlApi . '/edit-tag?pos=0&client=' . $this->userAgent;
    $data = 'a=user/-/state/com.google/broadcast&async=true&s='.$stream.'&i='.$id.'&T='.$this->token;
    return $this->post_url($url,$data);
}

This last one applies a review tag:

function set_article_review($id,$stream) {
    $url = $this->_urlApi . '/edit-tag?pos=0&client=' . $this->userAgent;
    $data = 'a=user/'.$this->userInfo->userId.'/label/Review&async=true&s='.$stream.'&i='.$id.'&T='.$this->token;
    return $this->post_url($url,$data);
}

The above should be used in the app so you can mark something to review when next using a regular browser.


Step 4: On to the Application

The application interface is built on the base of the excellent HTML5 boilerplate at http://html5boilerplate.com/. I took the index.php from the boilerplate and split it into header and footer include files. The main page is index.php and will accept an &list querystring value denoting which list to display. It defaults to the folder list. Any item in the list will go to articles.php with the &stream and &list passed. The class is placed in a file gr.php with gr-config.php actually being the file that includes it into the others. It might be obvious, but in gr-config.php we'll also put in the config options for the class. The last php file is set_article.php. It will be called with an AJAX request from articles.php and is responsible for calling the set functions.

I've included the two tags below in the header.php include file but they are commented out:

  <link rel="apple-touch-startup-image" href="splashimage.png">
  <meta name="apple-mobile-web-app-capable" content="yes" />

They make the web app function more like a regular app when you add the site on the iOS homescreen. apple-touch-startup-image provides an image to display while the main page is being loaded. apple-mobile-web-app-capable makes the site run on its own Safari instance rather then in the general Safari instance. The only problematic thing is that with apple-mobile-web-app-capable set to yes, clicking a link to a different domain opens the link in the main Safari instance, thus forcing you to multitask back to the reader app.


Step 5: Building the Main Page

For this application we are going to build a main page that will display a list of either Folders (default), Friends or Subscriptions.

Screenshot

The Setup

Here's the setup at the beginning of the index.php, articles.php, and set_article.php pages:

session_set_cookie_params(300);
session_start();
include_once('gr-config.php');
include_once('header.php');

The Menu

The menu is a simple HTML list that can be used to retrieve the different subscription lists:

<div id="menu">
    <ul>
        <li class="tags" onclick="window.location='index.php?list=tags'"></li>
        <li class="subs" onclick="window.location='index.php?list=subs'"></li>
        <li class="friends" onclick="window.location='index.php?list=friends'"></li>
    </ul>
</div>

The menu uses a sprite for the buttons based off the nice icon set Glyphish.

The Code

Now on to the first real use of the class. Basically, we are going to check if &list is set and, if not, default to tags view. Then it's just a switch statement to know what list to build and gr function to use.

First, we default $list to tags if nothing is passed and then pass $list to a switch. When $list matches subs we just output a pretty straight list of all the subscriptions returned. This can be a lot for heavy users which is why tags mode is the default.

if (isset($_GET['list'])) { $list = $_GET['list']; } else { $list = 'tags'; }
switch ($list) {
   case 'subs':
      $subs = $gr->get_subscriptions();
      foreach($subs->subscriptions as $sub) {
         echo '<li onclick="window.location = \'articles.php?list=',$list,'&stream=',$sub->id,'\'">  ',$sub->title,'<div class="right-arrow"></div></li>';
      }
      break;

The data comes back with the full label string rather then just the folder name. Setting up $remove will let us get rid of the unneeded parts of the label string and just show the folder name. Unfortunately, there is no way to exclude Review from the labels returned so I had to handle it with PHP. I also check to make sure we only display labels:

case 'tags':
    $tags = $gr->get_tags();
    $remove = 'user/'.$gr->userInfo->userId.'/label/';

    foreach($tags->tags as $tag) {
        if (strpos($tag->id,'/label/') > 0 && strpos($tag->id,'/Review') == 0) {
            $title = str_replace($remove,'',$tag->id);
            echo '<li onclick="window.location = \'articles.php?list=',$list,'&stream=',$tag->id,'\'">  ',$title,'<div class="right-arrow"></div></li>';
        }
    }
    
    break;

The friends URL though has a lot of additional information from the user's profile. One nice thing we can use is the profile picture and include it as a 32 pixel image:

case 'friends':
    $friends = $gr->get_friends();
    foreach($friends->friends as $friend) {
        if ($friend->contactId != -1) {
            echo '<li onclick="window.location = \'articles.php?list=',$list,'&stream=',$friend->stream,'\'"><img class="friends_icon" src="http://s2.googleusercontent.com'.$friend->photoUrl.'?sz=32">',$friend->displayName,'<div class="right-arrow"></div></li>';
        }
    }
    break;
}

Step 6: Building the Article Page

This page is where you'll view the articles, mark them read, set them to be reviewed later, mark them as a favorite, or share them with your friends.

Screenshot

The Setup

Same as the main page.

The Menu

Another simple HTML list. I'm not a fan of using window.history for a back button, so for the back I'm sending them back to the list they were viewing.

    <div id="menu">
      <ul>
         <li class="back" onclick="window.location = '/index.php?list=<?php echo $_GET['list'] ?>'"></li>
         <li> </li>
         <li class="read" onclick="set_article('read')"></li>
         <li class="starred" onclick="set_article('starred')"></li>
         <li class="broadcast" onclick="set_article('broadcast')"></li>
         <li class="review" onclick="set_article('review')"></li>
      </ul>
   </div>

The Code

The get_stream_items function from our class is the primary driver for the page and most of the defaults are good for this. By default it excludes items marked as read but I've extended the $xt_a array to also exclude articles marked with the "review" label/tag. This way items we mark for review never show up on this list even though we haven't read them yet. We are going to load up the articles in hidden div tags and only show the :first-child, then manipulate the others via JavaScript.

   $stream = $_GET['stream']; 
   $articles = $gr->get_stream_items($stream,array('user/-/state/com.google/read','user/'.$gr->userInfo->userId.'/label/Review'));
   if (count($articles->items) > 0) {
      foreach ($articles->items as $article) {

Instead of trying to hold state in a session or an array in JavaScript, I'm just using the relavant pieces of data as attributes on each article's wrapper div.

    echo '<div class="article" id="',$article->id,'" stream=',$article->origin->streamId,'>';
    echo '<div class="article-title"><a target="_blank" href="',$article->alternate[0]->href,'">',$article->title,'</a></div>';

The content can come back in summary form or not depending on the feed. Since we're mobile will try for summary but fall back to full content if it's not available. Also we'll make the links in the article pop in a new window.

    if (isset($article->summary->content)) {
        $content = str_replace('href=','target="_blank" href=',$article->summary->content);
    } else {
        $content = str_replace('href=','target="_blank" href=',$article->content->content);
    }

Finally, display what we have:

    echo '<div class="article-summary">',$content,'</div>';
    echo '</div>';
}

Step 7: JavaScript for Article Actions

The JavaScript is heavily reliant on jQuery. The article.php menu calls set_article() when you click on an action and calls set_article.php to process that action. set_article() will first grab the id and stream from the article we're viewing's div tag. It will then grab the total to fire off a reload if we hit the end of the available articles. The set_article.php script will return a 1 if the action performed should move to the next article. Otherwise, it checks the action performed and either lights up the Star or the Share icon. It's using $.eq() and the variable slice to enumerate through the returned articles.

Setting total variable dynamically rather then just using '20':

   var total = $('.article').size();
   function set_article(action){

Adding attributes to the article wrapper div tag:

    id = $('.article').eq(slice).attr('id');
    stream = $('.article').eq(slice).attr('stream');

Here we pass the id and stream to the set_article.php page and get back a 1 or 0. The 1 means the action requires the removal of the article from view and the 0 means this is just a state change.

    $.get('/set_article.php?action='+action+'&id='+id+'&stream='+stream,function(data){
    if (data=="1") {
        $('.article').eq(slice).hide();
        slice += 1;
        $('.article').eq(slice).show();

        $('.starred').css({backgroundPosition: '-62px -31px'});
        $('.broadcast').css({backgroundPosition: '-93px -31px'});
    } 
    else 
    {

If it returns a 0, then we need to see what action was taken so the proper icon state is also switched:

         if (action == 'starred') {
             $('.starred').css({backgroundPosition: '-62px 0px'});
         } else {
             $('.broadcast').css({backgroundPosition: '-93px 0px'});
         }
     }
        
     if (slice >= total) {
        window.location.reload(true);
     }
         
  });
}

Step 8: Applying Actions to Articles

This is the final bit of PHP that will allow us to manipulate the article.

The Setup

Same as the main page.

The Code

The hard work is done in the class, so this page is really just a switch to run the class function that matches up with the action. You might wonder why Google requires the $stream variable. From what I can tell, it all has to do with how Google stores all the reader data and cuts the transaction time when processing these edit functions.

if (isset($_GET['action'])) { 
   $action = $_GET['action']; 
   $id = $_GET['id']; 
   $stream = $_GET['stream']; 

   switch ($action) {
      case 'read':
         $r = $gr->set_article_read($id,$stream);
         echo '1';
         break;
      case 'starred':
         $r = $gr->set_article_starred($id,$stream);
         echo '0';
         break;
      case 'broadcast':
         $r = $gr->set_article_broadcast($id,$stream);
         echo '0';
         break;
      case 'review':
         $r = $gr->set_article_review($id,$stream);
         echo '1';
         break;
   }
}

Step 9: Rounding It Out With CSS

Most of the CSS isn't anything too special. I have the two key styles included below:

.article-summary  img, .article-summary  embed {
   max-height:50%;
   max-width:50%;
}

.article:first-child { display:block; }

The max-height:50% and max-width:50% declarations are very useful when repurposing HTML that may have images and embeds. It keeps the images and youtube includes from overloading the mobile screen.


Step 10: Learn More

Until there is an official API, these places are great to get some additional information on building apps with Google Reader:

Advertisement