Pages

Friday, May 25, 2012

0

Extending Yahoo! Pipes with Google App Engine

In my Uploads and Favorites pipe I wanted to turn the timestamps into so called "humanized dates", like these six examples:

23 seconds ago
1 minute ago
4 hours ago
2 days ago
7 months ago
1 year ago

This could not be done by using only Yahoo! Pipes.

Google App Engine to the rescue

Yahoo! Pipes can be extended using a web service, which accepts the feed items as json, and outputs them (after changes) as json. I used this blog article Using Google App Engine to Extend Yahoo! Pipes as a starting point. Django framework has date humanizing functions coded in Python, and I borrowed those functions from there. Then I created a new Google App Engine application "humandate", and made files app.yaml and humandate.py.

app.yaml

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. application: humandate
  2. version: 1
  3. runtime: python
  4. api_version: 1
  5.  
  6. handlers:
  7. - url: /favicon\.ico
  8.   static_files: favicon.ico
  9.   upload: favicon\.ico
  10.  
  11. - url: .*
  12.   script: humandate.py


humandate.py

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. #!/usr/bin/env python
  2. #
  3. # Humandate - a web service to humanize dates with Yahoo! Pipes
  4. # MS-potilas 2012. See http://yabtb.blogspot.com/2012/05/extending-yahoo-pipes-with-google-app.html
  5. #
  6. from google.appengine.ext import webapp
  7. from google.appengine.ext.webapp import util
  8. import simplejson
  9. #
  10. # MS-potilas: time humanizing code begins, borrowed from Django's "contrib.humanize".
  11. #
  12. import time
  13. from datetime import datetime, timedelta, date
  14.  
  15. def _now():
  16.     return datetime.now()
  17.  
  18. def abs_timedelta(delta):
  19.     """Returns an "absolute" value for a timedelta, always representing a
  20.    time distance."""
  21.     if delta.days < 0:
  22.         now = _now()
  23.         return now - (now + delta)
  24.     return delta
  25.  
  26. def date_and_delta(value):
  27.     """Turn a value into a date and a timedelta which represents how long ago
  28.    it was.  If that's not possible, return (None, value)."""
  29.     now = _now()
  30.     if isinstance(value, datetime):
  31.         date = value
  32.         delta = now - value
  33.     elif isinstance(value, timedelta):
  34.         date = now - value
  35.         delta = value
  36.     else:
  37.         try:
  38.             value = int(value)
  39.             delta = timedelta(seconds=value)
  40.             date = now - delta
  41.         except (ValueError, TypeError):
  42.             return (None, value)
  43.     return date, abs_timedelta(delta)
  44.  
  45. def naturaldelta(value, months=True, onlyoneyear=True):
  46.     """Given a timedelta or a number of seconds, return a natural
  47.    representation of the amount of time elapsed.  This is similar to
  48.    ``naturaltime``, but does not add tense to the result.  If ``months``
  49.    is True, then a number of months (based on 30.5 days) will be used
  50.    for fuzziness between years.
  51.  
  52.    MS-potilas: if ``onlyoneyear`` is True, no fuzziness between years 1-2.
  53.    Changed "an hour" to "1 hour" etc. (don't change "a moment"!) """
  54.  
  55.     date, delta = date_and_delta(value)
  56.     if date is None:
  57.         return value
  58.     use_months = months
  59.     seconds = abs(delta.seconds)
  60.     days = abs(delta.days)
  61.     years = days // 365
  62.     days = days % 365
  63.     months = int(days // 30.5)
  64.     if not years and days < 1:
  65.         if seconds == 0:
  66.             return "a moment"
  67.         elif seconds == 1:
  68.             return "1 second"
  69.         elif seconds < 60:
  70.             return "%d seconds" % (seconds)
  71.         elif 60 <= seconds < 120:
  72.             return "1 minute"
  73.         elif 120 <= seconds < 3600:
  74.             return "%d minutes" % (seconds // 60)
  75.         elif 3600 <= seconds < 3600*2:
  76.             return "1 hour"
  77.         elif 3600 < seconds:
  78.             return "%d hours" % (seconds // 3600)
  79.     elif years == 0:
  80.         if days == 1:
  81.             return "1 day"
  82.         if not use_months:
  83.             return "%d days" % days
  84.         else:
  85.             if not months:
  86.                 return "%d days" % days
  87.             elif months == 1:
  88.                 return "1 month"
  89.             else:
  90.                 return "%d months" % months
  91.     elif years == 1:
  92.         if onlyoneyear:
  93.             return "1 year"
  94.         if not months and not days:
  95.             return "1 year"
  96.         elif not months:
  97.             return "1 year, %d days" % days
  98.         elif use_months:
  99.             if months == 1:
  100.                 return "1 year, 1 month"
  101.             else:
  102.                 return "1 year, %d months" % months
  103.         else:
  104.             return "1 year, %d days" % days
  105.     else:
  106.         return "%d years" % years
  107.  
  108. def naturaltime(value, future=False, months=True):
  109.     """Given a datetime or a number of seconds, return a natural representation
  110.    of that time in a resolution that makes sense.  This is more or less
  111.    compatible with Django's ``naturaltime`` filter.  ``future`` is ignored for
  112.    datetimes, where the tense is always figured out based on the current time.
  113.    If an integer is passed, the return value will be past tense by default,
  114.    unless ``future`` is set to True."""
  115.     now = _now()
  116.     date, delta = date_and_delta(value)
  117.     if date is None:
  118.         return value
  119.     # determine tense by value only if datetime/timedelta were passed
  120.     if isinstance(value, (datetime, timedelta)):
  121.         future = date > now
  122.     ago = 'from now' if future else 'ago'
  123.     delta = naturaldelta(delta)
  124.     if delta == "a moment":
  125.         return "now"
  126.     return "%s %s" % (delta, ago)
  127. #
  128. # MS-potilas: time humanizing code ends.
  129. #
  130.  
  131. class MainHandler(webapp.RequestHandler):
  132.     def get(self):
  133.         try:
  134.             utime = int(self.request.get("unixtime"))
  135.             if utime > 0:
  136.                 dt = datetime.fromtimestamp(utime)
  137.                 self.response.headers['Content-Type'] = 'text/plain'
  138.                 self.response.out.write(naturaltime(dt))
  139.         except:
  140.             self.response.headers.add_header("Cache-Control", "public, max-age=14400")
  141.             self.response.out.write('<html><head><link rel="shortcut icon" href="/favicon.ico" /><title>Humanized Dates Web Service</title></head><body>Works either as Yahoo! Pipes <a target="_blank" href="http://pipes.yahoo.com/pipes/docs?doc=operators#WebService">web service</a>: makes a new field "when" based on "y:published.utime",<br />or by giving url parameter unixtime, <a href="/?unixtime=1337752504">example</a>. By MS-potilas 2012. See <a href="http://yabtb.blogspot.com/2012/05/extending-yahoo-pipes-with-google-app.html">yabtb.blogspot.com</a>.')
  142.             self.response.out.write('</body></html>')
  143.         return 0;
  144.  
  145.     def post(self):
  146.         try:
  147.             data = self.request.get("data")
  148.             items = simplejson.loads(data)
  149.             items = items["items"]
  150.  
  151.             for item in items:
  152.                 utime = int(item['y:published']['utime'])
  153.                 dt = datetime.fromtimestamp(utime)
  154.                 item['when'] = naturaltime(dt)
  155.  
  156.             self.response.content_type = "application/json"
  157.             simplejson.dump(items, self.response.out)
  158.         finally:
  159.             return 0;
  160.  
  161. def main():
  162.     application = webapp.WSGIApplication([('/', MainHandler)],
  163.                                          debug=True)
  164.     util.run_wsgi_app(application)
  165.  
  166. if __name__ == '__main__':
  167.     main()


I put these two files in a directory called "humandate" under my Google App Engine directory, with favicon.ico (the GAE default icon), and the development environment is ready. I now can deploy the application using "./appcfg.py update humandate/" or test it locally using "./dev_appserver.py humandate/", then the app runs at http://localhost:8080.

Testing the date humanizer

I made also an url based interface to "humandate" in addition to Yahoo! Pipes json interface. Url based interface works by giving a unix timestamp as "unixtime" parameter, like this. This made testing the date humanizer easier.

Using the service in Yahoo! Pipes

This service expects items to have y:published.utime field present. Service reads it from every item, turns it into a humanized date, and makes a new field "when" containing the humanized date. Then the service returns the items back to pipes.



See also my previous article about the four youtube pipes I've made (one of them uses this humandate, and one uses it indirectly). And have fun with Google App Engine and Yahoo! Pipes.

Thursday, May 24, 2012

0

Yahoo! Pipes, pipes, and more pipes for YouTube

Lately I've been playing around with Yahoo! Pipes and learned some new things. I already presented the YouTube Activity Feed, which I've cleaned up a little. And I've made three more pipes.


YouTube Activity Feed

Pipe ID: 58c841d14337ba4fbf693abd9701dc49
Pipe Web Address: http://pipes.yahoo.com/mspotilas/youtubeactivityfeed

Here's my previous article about this feed. YouTube API 2.0 does provide activity feeds, but you need to register an application to get developer key. My idea was to read the same info from user's channel feed page in html, parse the html and republish as a feed, and YouTube Activity Feed pipe does that.

As html parsing is quite complex task, if YouTube changes the feed page structure a lot, this pipe might break. I'll probably try to fix it if that happens.


YouTube Likes Feed

Pipe ID: a58d86555f7eb396e73d58ff50edc8f1
Pipe Web Address: http://pipes.yahoo.com/mspotilas/youtubelikesfeed

Same idea as in Youtube Activity Feed, but it gets only "liked videos" from the channel feed page. It is a bit simpler to parse only the likes. Also with this pipe, if YouTube changes the feed page a lot, the pipe might break.


YouTube Uploads and Favourites

Pipe ID: 0c2ee991c8d94ed0ceb19865c252e047
Pipe Web Address: http://pipes.yahoo.com/mspotilas/youtubeuploadsfavoritesfeed

YouTube has gdata feeds for user uploads and user favorites.

http://gdata.youtube.com/feeds/base/users/USERNAME/uploads?alt=rss&v=2
and
http://gdata.youtube.com/feeds/base/users/USERNAME/favorites?alt=rss&v=2

This pipe combines these two feeds, descriptions will be for example "2 days ago USERNAME favorited" or "1 hour ago USERNAME uploaded". The tricky part was to get this humanized date "2 days ago" derived from feed item's timestamp. It could not be done in Pipes only, so I extended its functionality with a web service I coded into Google AppEngine. It goes through the list, converts "y:published.utime" to humanized date like "2 days ago" and stores it in field "when" and returns the list back to pipes. Here's an article about the web service: Extending Yahoo! Pipes with Google App Engine.

This pipe should be robust, as it uses feeds as source, not page html.


YouTube Feed

Pipe ID: 73c5ee4a46e1837d908deaa60b1858d2
Pipe Web Address: http://pipes.yahoo.com/mspotilas/youtubefeed

This fourth pipe simply combines Uploads and Favourites and Likes Feed. Structure is very simple. Output is something similar to YouTube Activity feed, but it does not contain comments, playlist additions nor channel subscriptions. But sometimes simpler is better.


All pipes are compatible with my gadget which works also on Opera (Yahoo! Pipes badge does not). Just change the pipe ID (_id) to use a different pipe.

Feel free to examine these pipes, you can also make clones which you can modify. Comments are welcome, too. :)

Monday, May 21, 2012

10

Another gadget for YouTube Activity Feed, works with Opera

Few days ago I finished making my first Yahoo! Pipe, which collects info from YouTube channel feed page, and republishes that info. Yesterday I noticed, that Yahoo! Pipes badge output does not support Opera – with Opera, nothing is shown. It is strange that Yahoo does not fix this, maybe it is because Pipes is a free product. About 3 % of my blogs readers use Opera.

I knew I could make a gadget that works on Opera, too. Yesterday evening I coded first a javascript version, and today morning a little tidier jQuery version.


Installation is simple: add an HTML/Javascript gadget and put the following code in it:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. <script src='http://code.jquery.com/jquery-latest.js' type='text/javascript'></script>
  2. <style type="text/css">
  3. div.ytc {clear:both;padding:5px;font-size:11px;}
  4. div.ytc.odd {background-color: #eee;}
  5. div.ytc_thumb {position:relative;float:right;margin-left:4px;line-height:1;}
  6. div.ytc_thumb img {width:69px;height:52px;border:1px solid #888;}
  7. div.ytc_title {font-weight:bold;}
  8. </style>
  9. <script type='text/javascript'>
  10. // YouTube Activity Feed Gadget by MS-potilas 2012
  11. // See http://yabtb.blogspot.com/2012/05/another-gadget-for-youtube-activity.html
  12. // configuration:
  13. var ytfUserName = "mspotilas";
  14. var ytfMaxResults = 8;
  15. var ytfAllow = "";
  16. var ytfDisallow = "";
  17. var ytfEmpty = "No entries";
  18. //
  19. $(document).ready(function() {
  20.   $.getJSON("http://run.pipes.yahoo.com/pipes/pipe.run?_id=58c841d14337ba4fbf693abd9701dc49&_render=json&max-results="+ytfMaxResults+"&allow="+ytfAllow+"&disallow="+ytfDisallow+"&user="+ytfUserName+"&_callback=?", function(response) {
  21.     var htm = "";
  22.     for(var i=0;i<response.count;i++) {
  23.       var item = response.value.items[i];
  24.       htm += '<div class="ytc';
  25.       if(i%2 == 1) htm += ' odd';
  26.       htm += '"><div class="ytc_thumb"><a target="_blank" href="' + item.link + '"><img title="' + item.title + '" src="' + item.thumb + '"/></a></div>';
  27.       htm += '<div class="ytc_title"><a target="_blank" href="' + item.link + '">' + item.title + '</a></div>';
  28.       htm += '<div class="ytc_description">' + item.description + '</div><div style="clear:both;"></div></div>';
  29.     }
  30.     if(htm == "") htm = ytfEmpty;
  31.     $("#ytfeed").html(htm);
  32.   });
  33. });
  34. </script>
  35. <div id="ytfeed">Loading...</div>


You can leave out the first line, if your template already loads jQuery. Make necessary configuration changes (change YouTube user name, for example). I did only very basic CSS, you can of course modify it to your liking. Code can be used also outside Blogger (in any html page).

Friday, May 18, 2012

4

Yahoo! Pipe to show YouTube activity feed in Blogger, WordPress, etc.

I've had this idea for some time: somehow republish YouTube channel activity feed page as an RSS feed. So you could get a view of your latest YouTube activity in your blog, for example. This week I finally implemented a version of this using Yahoo! Pipes. This is a completely new gadget for Blogger, I have not seen anything like this before. You can use the pipe gadget output on WordPress and other internet pages, too. And you can get the RSS address for the pipe to connect it with various services and/or applications.

Link to the pipe: pipes.yahoo.com/mspotilas/youtubeactivityfeed.

And here's the "badge output" of that pipe from my youtube feed page, with 5 items:


This can be put into a sidebar gadget, like I've done in YABTB demo blog. The code to put inside an HTML/Javascript gadget is simple:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. <!--  YouTube activity feed by MS-potilas 2012,
  2.  --   see http://yabtb.blogspot.com/2012/05/yahoo-pipe-to-show-youtube-activity.html
  3. -->
  4. <script src="http://l.yimg.com/a/i/us/pps/listbadge_1.6.js">{"pipe_id":"58c841d14337ba4fbf693abd9701dc49","_btype":"list","pipe_params":{"disallow":"","allow":"","user":"mspotilas","max-results":"5"},"height":"100%","hideHeader":true}</script>


You may also use the "Get as a Badge" link when the pipe is run in the pipe's page, it offers an automated gadget installation into Blogger blogs, WordPress blogs and others.

Update May 21th: Because Badge output does not work with Opera browser, you might consider using instead a gadget made by me, which works with Opera.

Parameters of the pipe:

user: youtube user name,
max-results: maximum number of items returned,
allow: only accept entries that have this word in description, for example "liked" to get only likes or "uploaded" to get only uploads,
disallow: don't allow entries that have this word in description, for example "commented" to filter out commenting activity.

So at minimum you just have to change the user parameter value from "mspotilas" to any youtube user name you want (it does not have to be yours, try for example "NBC"). In my YABTB demo blog, to show what can be done with CSS styles, I also changed background colors, adjusted font sizes and margins, changed thumbnail to right side, changed thumbnail proportions (size), and hid the footer (powered by and get this), like this:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. <!--  YouTube activity feed by MS-potilas 2012,
  2.  --   see http://yabtb.blogspot.com/2012/05/yahoo-pipe-to-show-youtube-activity.html
  3. -->
  4. <style type="text/css">
  5. li.ybi {background-color: #101010 !important; }
  6. li.ybi.odd {background-color: #202020 !important; }
  7. .pipesTitle {font-size: 11px !important; }
  8. .pipesDescription {font-size: 10px !important; }
  9. .pipesThumbnail {float: right !important; margin-right: -2px !important; margin-left: 4px !important;}
  10. .pipesThumbnail img { width: 69px !important; height: 52px !important; border: 1px solid #888 !important;}
  11. .pipesText {margin-left: 6px !important;}
  12. .pipesHolder {margin: -3px 0 -3px 0 !important;}
  13. div.ybf { display: none !important; }
  14. </style>
  15. <script src="http://l.yimg.com/a/i/us/pps/listbadge_1.6.js">{"pipe_id":"58c841d14337ba4fbf693abd9701dc49","_btype":"list","pipe_params":{"disallow":"","allow":"","user":"mspotilas","max-results":"8"},"height":"100%","hideHeader":true}</script>


There is also a YouTube RSS feed, but... YouTube gdata feed for YouTube channel includes only uploads. No likes, favourites, comments, etc. Link to YouTube user's RSS feed of uploads is:

http://gdata.youtube.com/feeds/base/users/youtubeusername/uploads?alt=rss&v=2&orderby=published

For example my YouTube channel's RSS feed is:



This is my very first Yahoo Pipe, so the implementation might be a bit clumsy. But it works, at least in my tests. :) You can clone the pipe and modify it according to your needs, or use this pipe if it suits you as is. If you like it, or find some bugs or something to improve, please tell me.

Update May 20th: I found out that Opera browser is not supported by Yahoo! Pipes. :( I think I could make a gadget for the pipe which would work in Opera, too, but I'm not sure if I will. About 3 % of my blog's viewers do use Opera.
Update May 21th: Here's the gadget I made, works with Opera, too.

Wednesday, May 2, 2012

36

Top Commentators Gadget with avatars

I had a request of this gadget in one blog comment. There are some top commentators gadgets around, most relying on a Yahoo pipe. I knew it can be done with just javascript, with no pipes. Demo: it is installed on this blog, look at the right sidebar. Hope you like it. :)

Features

You can configure the maximum number of top commentators, and minimum number of comments one must have to get on the list (to weed out the 1 comment commentators, if wanted, for example). Own nick and a list of other nicks (for example "Anonymous") can be excluded from the list. Output strings can be translated.

The Script, installing

Add a HTML/Javascript gadget to your blog, and paste the following code into it.

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. <style type="text/css">
  2. .top-commenter-line {margin: 3px 0;}
  3. .top-commenter-line .profile-name-link {padding-left:0;}
  4. .top-commenter-avatar {vertical-align:middle;}
  5. </style>
  6. <script type="text/javascript">
  7. //
  8. // Top Commentators gadget with avatars, by MS-potilas 2012.
  9. // Gets a list of top commentators from all comments, or specified number of days in the past.
  10. // See http://yabtb.blogspot.com/2012/05/top-commenters-gadget-with-avatars.html
  11. //
  12. // CONFIG:
  13. var maxTopCommenters = 5;   // how big a list of top commentators
  14. var minComments = 1;        // how many comments must top commentator have at least
  15. var numDays = 0;            // from how many days (ex. 30), or 0 from "all the time"
  16. var excludeMe = true;       // true: exclude my own comments
  17. var excludeUsers = ["Anonymous", "someotherusertoexclude"];     // exclude these usernames
  18. var maxUserNameLength = 42; // 0: don't cut, >4: cut usernames
  19. //
  20. var txtTopLine = '<b>[#].</b> [image] [user] ([count])';
  21. var txtNoTopCommenters = 'No top commentators at this time.';
  22. var txtAnonymous = '';      // empty, or Anonymous user name localized if you want to localize
  23. //
  24. var sizeAvatar = 16;
  25. var cropAvatar = true;
  26. //
  27. var urlNoAvatar = "http://lh4.googleusercontent.com/-069mnq7DV_g/TvgRrBI_JaI/AAAAAAAAAic/Iot55vywnYw/s"+sizeAvatar+"/avatar_blue_m_96.png"; // http://www.blogger.com/img/avatar_blue_m_96.png resizeable
  28. var urlAnoAvatar = 'http://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm&s=' + sizeAvatar;
  29. var urlMyProfile = ''; // set if you have no profile gadget on page
  30. var urlMyAvatar = '';  // can be empty (then it is fetched) or url to image
  31. // config end
  32. // for old IEs & IE modes:
  33. if(!Array.indexOf) {
  34.  Array.prototype.indexOf=function(obj) {
  35.   for(var i=0;i<this.length;i++) if(this[i]==obj) return i;
  36.   return -1;
  37. }}
  38. function replaceTopCmtVars(text, item, position)
  39. {
  40.   if(!item || !item.author) return text;
  41.   var author = item.author;
  42.  
  43.   var authorUri = "";
  44.   if(author.uri && author.uri.$t != "")
  45.     authorUri = author.uri.$t;
  46.  
  47.   var avaimg = urlAnoAvatar;
  48.   var bloggerprofile = "//www.blogger.com/profile/";
  49.   if(author.gd$image && author.gd$image.src && authorUri.indexOf(bloggerprofile) >= 0)
  50.     avaimg = author.gd$image.src;
  51.   else {
  52.     var parseurl = document.createElement('a');
  53.     if(authorUri != "") {
  54.       parseurl.href = authorUri;
  55.       avaimg = 'http://www.google.com/s2/favicons?domain=' + parseurl.hostname;
  56.     }
  57.   }
  58.   if(urlMyProfile != "" && authorUri == urlMyProfile && urlMyAvatar != "")
  59.     avaimg = urlMyAvatar;
  60.   if(avaimg == "http://img2.blogblog.com/img/b16-rounded.gif" && urlNoAvatar != "")
  61.     avaimg = urlNoAvatar;
  62.   var newsize="s"+sizeAvatar;
  63.   avaimg = avaimg.replace(/\/s\d\d+-c\//, "/"+newsize+"-c/");
  64.   if(cropAvatar) newsize+="-c";
  65.   avaimg = avaimg.replace(/\/s\d\d+(-c){0,1}\//, "/"+newsize+"/");
  66.  
  67.   var authorName = author.name.$t;
  68.   if(authorName == 'Anonymous' && txtAnonymous != '' && avaimg == urlAnoAvatar)
  69.     authorName = txtAnonymous;
  70.   var imgcode = '<img class="top-commenter-avatar" height="'+sizeAvatar+'" width="'+sizeAvatar+'" title="'+authorName+'" src="'+avaimg+'" />';
  71.   if(authorUri!="") imgcode = '<a href="'+authorUri+'">'+imgcode+'</a>';
  72.  
  73.   if(maxUserNameLength > 3 && authorName.length > maxUserNameLength)
  74.     authorName = authorName.substr(0, maxUserNameLength-3) + "...";
  75.   var authorcode = authorName;
  76.   if(authorUri!="") authorcode = '<a class="profile-name-link" href="'+authorUri+'">'+authorcode+'</a>';
  77.  
  78.   text = text.replace('[user]', authorcode);
  79.   text = text.replace('[image]', imgcode);
  80.   text = text.replace('[#]', position);
  81.   text = text.replace('[count]', item.count);
  82.   return text;
  83. }
  84.  
  85. var topcommenters = {};
  86. var ndxbase = 1;
  87. function showTopCommenters(json) {
  88.   var one_day=1000*60*60*24;
  89.   var today = new Date();
  90.  
  91.   if(urlMyProfile == "") {
  92.     var elements = document.getElementsByTagName("*");
  93.     var expr = /(^| )profile-link( |$)/;
  94.     for(var i=0 ; i<elements.length ; i++)
  95.       if(expr.test(elements[i].className)) {
  96.         urlMyProfile = elements[i].href;
  97.         break;
  98.       }
  99.   }
  100.  
  101.   if(json && json.feed && json.feed.entry && json.feed.entry.length) for(var i = 0 ; i < json.feed.entry.length ; i++ ) {
  102.     var entry = json.feed.entry[i];
  103.     if(numDays > 0) {
  104.       var datePart = entry.published.$t.match(/\d+/g); // assume ISO 8601
  105.       var cmtDate = new Date(datePart[0],datePart[1]-1,datePart[2],datePart[3],datePart[4],datePart[5]);
  106.  
  107.       //Calculate difference btw the two dates, and convert to days
  108.       var days = Math.ceil((today.getTime()-cmtDate.getTime())/(one_day));
  109.       if(days > numDays) break;
  110.     }
  111.     var authorUri = "";
  112.     if(entry.author[0].uri && entry.author[0].uri.$t != "")
  113.       authorUri = entry.author[0].uri.$t;
  114.  
  115.     if(excludeMe && authorUri != "" && authorUri == urlMyProfile)
  116.       continue;
  117.     var authorName = entry.author[0].name.$t;
  118.     if(excludeUsers.indexOf(authorName) != -1)
  119.       continue;
  120.  
  121.     var hash=entry.author[0].name.$t + "-" + authorUri;
  122.     if(topcommenters[hash])
  123.       topcommenters[hash].count++;
  124.     else {
  125.       var commenter = new Object();
  126.       commenter.author = entry.author[0];
  127.       commenter.count = 1;
  128.       topcommenters[hash] = commenter;
  129.     }
  130.   }
  131.   if(json && json.feed && json.feed.entry && json.feed.entry.length && json.feed.entry.length == 200) {
  132.     ndxbase += 200;
  133.     document.write('<script type="text/javascript" src="http://'+window.location.hostname+'/feeds/comments/default?redirect=false&max-results=200&start-index='+ndxbase+'&alt=json-in-script&callback=showTopCommenters"></'+'script>');
  134.     return;
  135.   }
  136.  
  137.   // convert object to array of tuples
  138.   var tuplear = [];
  139.   for(var key in topcommenters) tuplear.push([key, topcommenters[key]]);
  140.  
  141.   tuplear.sort(function(a, b) {
  142.     if(b[1].count-a[1].count)
  143.         return b[1].count-a[1].count;
  144.     return (a[1].author.name.$t.toLowerCase() < b[1].author.name.$t.toLowerCase()) ? -1 : 1;
  145.   });
  146.  
  147.   // list top topcommenters:
  148.   var realcount = 0;
  149.   for(var i = 0; i < maxTopCommenters && i < tuplear.length ; i++) {
  150.     var item = tuplear[i][1];
  151.     if(item.count < minComments)
  152.         break;
  153.     document.write('<di'+'v class="top-commenter-line">');
  154.     document.write(replaceTopCmtVars(txtTopLine, item, realcount+1));
  155.     document.write('</d'+'iv>');
  156.     realcount++;
  157.   }
  158.   if(!realcount)
  159.     document.write(txtNoTopCommenters);
  160. }  
  161. document.write('<script type="text/javascript" src="http://'+window.location.hostname+'/feeds/comments/default?redirect=false&max-results=200&alt=json-in-script&callback=showTopCommenters"></'+'script>');
  162. </script>


Script configuration

There are two CSS definitions at first, which can be changed to your liking. After that there comes javascript code and configurable variables. Here is a list of the variables and a short explanation.

maxTopCommenters: how big the list is at maximum
minComments: how many comments must top commentator have at least
numDays: from how many days (ex. 30), or 0 from "all the time" (max 500 comments)
excludeMe: true, if own comments excluded
excludeUsers: array of usernames to exclude
maxUserNameLength: if 0, don't cut; if > 4, cut lenghty usernames

txtTopLine: specifies, what is output on each line. This text can have simple variables, which are: [#], [image], [user], and [count]. [#] is substituted by position on the list, [image] by commentator's avatar image, [user] by commentator name and [count] by number of comments. Text can also contain html.
txtNoTopCommenters: what to display, if list is empty
txtAnonymous: "Anonymous" username localized, or empty if English text "Anonymous" is to be used
getTitles: if true, fetch titles from post feed; if false, titles are generated from url
blogger favico [B]
urlMyAvatar: especially if trueAvatars = false, set here the url to your avatar image
urlMyProfile: if you don't have a profile gadget on page, put your profile url here!
urlAnoAvatar: anonymous avatar, you can change from mystery man to a custom one, if you want, possibly this
urlNoAvatar: comment feed offers the icon (Blogger logo) for those Bloggers who have not set their profile image. You can override the icon with this setting (default: ).

cropAvatar: crop avatar to square (true) or stretch (false) to square
sizeAvatar: size of avatar in x and y direction, pixels

If you have very much comments (thousands and thousands of them), I suggest you to use this as "top commentators of 30 days" gadget or something like that (set numDays variable to 30), so that it won't take too long to load and calculate all the comments.

June 5th 2012: Small CSS update: Blogger changed the paddings of .profile-name-link, that's why I added this line (line 3.):
.top-commenter-line .profile-name-link {padding-left:0;}

August 1st 2017: To fix avatars, please change lines 48 and 49 (article code is updated).
See the hack
for this dynamic
views icon: