Pages

Wednesday, March 14, 2012

4

Google App Engine, Python 2.7, and lxml

Google App Engine (GAE) lets you build and run applications on Google's infrastructure. I have done two applications with GAE and Python (the programming language). The previous version of my tool to get blogger avatars (avafavico) used regular expressions to parse the Blogger profile page for the profile photo, or user's first contributed blog's favico, if no profile photo is found. As you might know, using regular expressions may not be the best solution. It would be better to parse html page document object model (DOM) and get results there. That way the code is not so sensitive to possible changes in the page html code. Of course I did the regular expressions so, that they should work with different html.

Previously I used GAE Python 2.5 environment, which was the default and supported version. On February 27th Python 2.7 became fully supported. GAE with Python 2.7 contains more external libraries than version 2.5, one of those libraries being lxml.

I tried to search for different solutions and examples, but there were not many. With GAE Python 2.5, one can use BeautifulSoup, but there are some issues (problematic 3.1.0 version, uncertain development future of BS, etc.). And there is minidom, but it may not handle broken html well. Blogger profile page should not have broken html, but you never know. lxml is definitely better and faster, supports XPath, etc.

The day before yesterday I updated the GAE app to use Python 2.7 and lxml. There were none to some examples about using python27, lxml and GAE, so I'll show you here a working example. First I started modifying the file app.yaml, there I changed runtime to python27, added "threadsafe" (false), and added (latest) lxml in libraries section. Increased version number to 5. Now app.yaml looks like this:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. application: avafavico
  2. version: 5
  3. runtime: python27
  4. api_version: 1
  5. threadsafe: false
  6.  
  7. default_expiration: "8h"
  8.  
  9. inbound_services:
  10. - warmup
  11.  
  12. handlers:
  13. - url: /favicon.ico
  14.   static_files: static/images/favicon.ico
  15.   upload: static/images/favicon.ico
  16. - url: /robots.txt
  17.   static_files: static/robots.txt
  18.   upload: static/robots.txt
  19. - url: .*
  20.   script: blogava.py
  21.  
  22. libraries:
  23. - name: lxml
  24.   version: latest

In blogava.py I added "from lxml import etree" and then used etree functions instead of regular expressions to find things in html. Here's how DOM tree is constructed, variable "result" contains page html as a string, and then XPath is applied to the tree, like this:

>>> tree = etree.HTML(result)
>>> r=tree.xpath("//img[@id='profile-photo']/@src")

Here find from the tree the first img tag, which id is set to "profile-photo", and get that tag's src attribute. In the full script, if no id='profile-photo' is not found, then try to search for the first image that has class "photo". If both fails, search for first "contributed-to" blog, and use it's favicon, if that is not found, use Blogger favicon. And here is the blogava.py source file:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. #!/usr/bin/env python
  2. #
  3. # "blogava" - blogger avatar fetch, and gradient data URI generator
  4. # v1 2.11.2011
  5. # v2 ..
  6. # v3 11.11.2011
  7. # v4 14.11.2011
  8. #    ?c1=f8dd99&c2=eeaa00&w=1&h=20 create gradient png
  9. #    optional: ?download or ?js[=variablename]
  10. # v4.01 14.12.2011 -
  11. #    blogger new profile page default profile without server-part,
  12. #    added code to deal with that
  13. # v4.02 18.12.2011 -
  14. #    increased timeout 5->15 in blogger profile pages, handle downloaderrors
  15. # v5 11.3.2012 -
  16. #    uses now python27 and lxml/etree/xpath to parse profile pages (is much
  17. #    better that regular expressions)
  18. #
  19. # -----------------------------------------------------------------------------
  20. #
  21. # fetch profile image from blogger user id. usage:
  22. # http://avafavico.appspot.com/?userid=01234567890
  23. #
  24. # See http://yabtb.blogspot.com/2011/11/google-app-engine-python-application.html
  25. #
  26. # This is like my first python app, so it may not be too shiny... but it works
  27. #
  28. #   - MS-potilas
  29. #
  30.  
  31. import sys
  32. import cgi
  33. import re
  34. import base64
  35. from google.appengine.api import images
  36. from google.appengine.api import urlfetch
  37. from google.appengine.api import memcache
  38. from google.appengine.runtime import DeadlineExceededError
  39. from lxml import etree
  40.  
  41. #
  42. # png & gradient code based on:
  43. # http://jtauber.com/blog/2008/05/18/creating_gradients_programmatically_in_python/
  44. #
  45.  
  46. #########################################
  47.  
  48. def make_png(width, height, rgba_func):
  49.   import zlib
  50.   import struct
  51.   import array
  52.  
  53.   rotate = False
  54.   if height < width:
  55.     width, height = height, width
  56.     rotate = True
  57.  
  58.   def make_chunk(chunk_type, data):
  59.     chu = struct.pack('!I', len(data)) + chunk_type + data
  60.     checksum = zlib.crc32(data, zlib.crc32(chunk_type))
  61.     chu += struct.pack('!I', 0xFFFFFFFF & checksum)
  62.     return chu
  63.  
  64.   def frange(x):
  65.     for i in xrange(x):
  66.       yield i/float(x)
  67.  
  68.   def get_data(width, height, rgba_func):
  69.     data = array.array('B')
  70.     for y in frange(height):
  71.       data.append(0)
  72.       for x in frange(width):
  73.         data.extend(int(round(v * 255)) for v in rgba_func(x, y))
  74.     return zlib.compress(data)
  75.  
  76.   out = array.array('B', [137, 80, 78, 71, 13, 10, 26, 10]).tostring() # PNG signature
  77.   color_type = 6 if len(list(rgba_func(0,0))) == 4 else 2
  78.   out += make_chunk('IHDR', struct.pack('!2I5B', width, height, 8, color_type, 0, 0, 0))
  79.   out += make_chunk('IDAT', get_data(width, height, rgba_func))
  80.   out += make_chunk('IEND', '')
  81.   img = images.Image(out)
  82.    
  83.   img.rotate(270)     # rotate it around to optimize
  84.   if not rotate:      # not rotate -> do full circle
  85.     img.rotate(90)
  86.   result = img.execute_transforms(output_encoding=images.PNG)
  87.   return result
  88.  
  89. def linear_gradient(start_value, stop_value, start_offset=0.0, stop_offset=1.0):
  90.   return lambda offset: (start_value + ((offset - start_offset) / (stop_offset - start_offset) * (stop_value - start_value))) / 255.0
  91.  
  92. def gradient(segments):
  93.   def gradient_function(x, y):
  94.     segment_start = 0.0
  95.     for segment_end, start, end in segments:
  96.       if y < segment_end:
  97.         return (linear_gradient(start[i], end[i], segment_start, segment_end)(y) for i in xrange(len(start)))
  98.       segment_start = segment_end
  99.   return gradient_function
  100.  
  101. #########################################
  102.  
  103. def getFavico(domain):
  104. # get favico for domain. first check cache
  105.   result = memcache.get(key=domain)
  106.   if result is None:
  107.     result = fetchUrl("http://www.google.com/s2/favicons?domain=" + domain)
  108.     memcache.add(key=domain, value=result, time=14400)
  109.   return result
  110.  
  111. def fetchUrl(url):
  112.   url = re.sub("^//", "http://", url)
  113.   dline = 5
  114.   if re.match("http:\/\/www\.blogger.com\/profile\/.+", url):
  115.     dline = 15
  116.   try:
  117.     result = urlfetch.fetch(url, deadline=dline)
  118.   except urlfetch.DownloadError:
  119.     return None
  120.   if result.status_code == 200:
  121.     return result.content
  122.   return None
  123.  
  124. #########################################
  125.  
  126. def main():
  127.   form = cgi.FieldStorage(keep_blank_values = True)
  128.   c1 = form.getfirst("c1")
  129.   c2 = form.getfirst("c2")
  130.   w = form.getfirst("w")
  131.   h = form.getfirst("h")
  132.  
  133.   if w is None or not re.match('\d+$', w):
  134.     w = 0
  135.   if h is None or not re.match('\d+$', h):
  136.     h = 0
  137.   if int(w)*int(h) > 4096:
  138.     print "Status: 400 Bad Request"
  139.     print
  140.     print "dimensions (w x h) too large"
  141.     return 1
  142.  
  143.   if c1 is not None and c2 is not None:
  144.     if re.match('[a-f0-9]{6}$', c1, re.I) and re.match('[a-f0-9]{6}$', c2, re.I) and int(w) > 0 and int(h) > 0:
  145.       c1r = int(c1[:2],16)
  146.       c1g = int(c1[2:][:2],16)
  147.       c1b = int(c1[-2:],16)
  148.       c2r = int(c2[:2],16)
  149.       c2g = int(c2[2:][:2],16)
  150.       c2b = int(c2[-2:],16)
  151.     else:
  152.       print "Status: 400 Bad Request"
  153.       print
  154.       print "invalid parameters"
  155.       return 1
  156.  
  157.     img = make_png(int(w), int(h), gradient([ (1.0, (c1r, c1g, c1b), (c2r, c2g, c2b)), ]))
  158.  
  159.     if form.getfirst("download") is not None:
  160.       print "Content-Type: image/png"
  161.       print "Cache-Control: public, max-age=86400"
  162.       print
  163.       print img
  164.       return 0
  165.     imgenc = base64.b64encode(img)
  166.     if form.getfirst("js") is not None:
  167.       var = form.getfirst("js")
  168.       if not re.match('[a-zA-Z]+$', var):
  169.         var = "result"
  170.      
  171.       print "Content-Type: text/javascript"
  172.       print "Cache-Control: public, max-age=86400"
  173.       print
  174.       print 'var ' + var + '="'+imgenc+'";'
  175.       return 0
  176.  
  177.     imgenc = 'data:image/png;base64,' + imgenc
  178.     print "Content-Type: text/html"
  179.     print "Cache-Control: public, max-age=86400"
  180.     print
  181.     print '<html><head><link rel="shortcut icon" href="/favicon.ico" /><title>Gradient PNG data URI generator</title></head><body style="text-align:center;">'
  182.     print 'Gradient from #' + c1 + ' to #' + c2 + ', width='+w+', height='+h+':<br />'
  183.     print '<textarea id="pngarea" rows="' + str(round(len(imgenc)/64+2))+ '" cols="64">'+imgenc+'</textarea>'
  184.     print '<script>document.getElementById("pngarea").focus();document.getElementById("pngarea").select();</script>'
  185.     print '<div style="margin:10px"><a title="Click to open data URI" target="_top" href="'+imgenc+'"><img src="'+imgenc+'" style="padding:10px;border:1px solid #d0d0d0;" /></a></div>'
  186.     print 'Use it for example like this in CSS:<br /><div style="margin-left:auto;margin-right:auto;width:520px;text-align:left;font-family:monospace;word-wrap:break-word;font-size:90%;margin-bottom:12px;">'
  187.     print 'background:url('+imgenc+') 100% 100%;'
  188.     print '</div>'
  189.     print '<hr />'
  190.     print '<div style="font-size:85%">by MS-potilas 2011-2012, see <a target="_top" href="http://yabtb.blogspot.com/2011/11/gradient-png-data-uri-maker-reference.html">yabtb.blogspot.com</a>.</div>'
  191.     print '</body></html>'
  192.     return 0
  193.  
  194.   userid = form.getfirst("userid")
  195.  
  196.   if userid is None:
  197.     print "Content-Type: text/html"
  198.     print "Cache-Control: public, max-age=14400"
  199.     print
  200.     print '<html><head><link rel="shortcut icon" href="/favicon.ico" /><title>Fetch Blogger Avatar</title></head><body style="text-align:center;">Application to return small icon from Blogger profile. Usage: ?userid=USERID.<br /><hr /><div style="font-size:85%">by MS-potilas 2011-2012, see <a href="http://yabtb.blogspot.com/2011/11/python-tool-to-get-blogger-avatar.html">yabtb.blogspot.com</a>.</div><br />'
  201.     print '</body></html>'
  202.     return 0
  203.  
  204.   if not re.match('\d+$', userid):
  205.     print "Status: 400 Bad Request"
  206.     print
  207.     print "invalid userid"
  208.     return 1
  209.  
  210.   # first check cache
  211.   thedata = memcache.get(key=userid)
  212.   if thedata is None:
  213.     domain = "www.blogger.com" # fallback favico
  214.     url = "http://www.blogger.com/profile/"+userid
  215.     result = fetchUrl(url)
  216.     if result is not None:
  217.       tree = etree.HTML(result)
  218.       r=tree.xpath("//img[@id='profile-photo']/@src")
  219.       if len(r) == 0:
  220.         r=tree.xpath("//img[@class and contains(concat(' ',normalize-space(@class),' '),' photo ')]/@src")
  221.       if len(r) > 0:
  222.         found = r[0]
  223.       # if profile photo not found, search person's first blog's address (rel="contributor-to")
  224.       if found is None:
  225.         r=tree.xpath("//a[@rel and contains(concat(' ',normalize-space(@rel),' '),' contributor-to ')]/@href")
  226.         if len(r) > 0:
  227.           found = r[0]
  228.         if found is not None:
  229.           domain = re.search('(http://){0,1}(.+?)[$/]', found).group(2)
  230.           found = None
  231.       if found is None:
  232.         result = getFavico(domain)
  233.       else:
  234.         found = re.sub('^//',"http://", found)
  235.         if not re.match('http', found):
  236.           found = "http://www.blogger.com" + found
  237.         result = fetchUrl(found)
  238.     # if loading has failed, fallback to domain's favico
  239.     if result is None:
  240.       result = getFavico(domain)
  241.     if result is not None:
  242.       img = images.Image(result)
  243.  
  244.       if img.width > img.height*1.67:
  245.         img.crop(0.2,0.0,0.8,1.0)
  246.       elif img.height > img.width*1.67:
  247.         img.crop(0.0,0.2,1.0,0.8)
  248.       elif img.width > img.height*1.25:
  249.         img.crop(0.1,0.0,0.9,1.0)
  250.       elif img.height > img.width*1.25:
  251.         img.crop(0.0,0.1,1.0,0.9)
  252.  
  253.       img.resize(32,32)
  254.       thedata=img.execute_transforms(output_encoding=images.PNG)
  255.       memcache.add(key=userid, value=thedata, time=14400)
  256.     else:
  257.       print "Status: 404 Not Found"
  258.       print
  259.       print "not found"
  260.       return 1 #all fetches failed
  261.  
  262.   print "Content-Type: image/png"
  263.   print "Cache-Control: public, max-age=14400"
  264.   print
  265.   print thedata
  266.   return 0
  267.  
  268. if __name__ == '__main__':
  269.   main()

This new version of avafavico has been up and running for two days. I'm very pleased that I got lxml working with GAE, all in all it was quite easy. Hope this example is useful to someone. If it helped you, please leave a comment. :)

Thursday, March 8, 2012

3

YouTube/JSONP oEmbed service "oembed-js"

In the previous post I introduced my oembed-js service and an application using it: YouTube embeds in Blogger comments.

Oembed-js is a Google Appengine application coded in Python. It relays YouTube oembed calls and offers results also in JSONP format, so javascript applications can use it.

Parameters

url: the (youtube) url to embed (best to be urlencoded)
maxwidth: max width of embedded object (optional)
maxheight: max height of embedded object (optional)
format: json/jsonp or xml (optional, default:json/jsonp)
callback or jsonp: the javascript callback function, required, if jsonp format is wanted
callID: a call ID that is passed through to response (optional)

Sample call:

http://oembed-js.appspot.com/?callback=foo&callID=123&url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DGMra5Wh51oU

Response

YouTube oEmbed response in json(p) or xml format, including field html (see oembed specs), and:

callID: if callID was given, it is passed through (can identify the response)
url: pass through the url that is being embedded
videoid: contains 11 char long YouTube videoid, if single video embed
playlist: contains playlist id, if playlist embed

Sample response:

foo({"provider_url": "http://www.youtube.com/", "version": "1.0", "title": "Sviatoslav Richter in Kiev, 1958 - Pictures at an Exhibition", "url": "http://www.youtube.com/watch?v=GMra5Wh51oU", "type": "video", "videoid": "GMra5Wh51oU", "thumbnail_width": 480, "height": 344, "width": 459, "html": "<iframe width=\"459\" height=\"344\" src=\"http://www.youtube.com/embed/GMra5Wh51oU?fs=1&feature=oembed\" frameborder=\"0\" allowfullscreen></iframe>", "author_name": "FirstPublicChannel", "callID": "123", "provider_name": "YouTube", "thumbnail_url": "http://i4.ytimg.com/vi/GMra5Wh51oU/hqdefault.jpg", "thumbnail_height": 360, "author_url": "http://www.youtube.com/user/FirstPublicChannel"})

Another demo how to use it

Enter YouTube URL (video or playlist):

 

This demo and its source is also here in this jsFiddle.

Update March 27th 2012: Now oembed-js supports youtu.be urls (YouTube oembed service does not support them). Also now supports videos which have embedding turned off (YouTube oembed does not support these either).

Sunday, March 4, 2012

30

Allow users embed YouTube videos in Blogger comments

Some time ago while googling something about video embeds I came across with oEmbed specification. This simple API allows websites to display embedded content from oEmbed providers. After some inspecting I used the oEmbed in a Python script to turn youtube links in a text into embedded videos. Worked nicely.

oEmbed returns data in JSON or XML format. That is ok with server side languages like php and Python. But for javascript this is a problem. If there is a problem, usually solution is found, and I found Embedly service, which is unfortunately not free.

With no intention to invest any money, and hands itching for another Google Appengine Python app, I saw an opportunity. To make a service that relays YouTube's oEmbed results to javascript format (JSONP). And all with memory caching etc. so it should be able to handle some traffic. After some coding it was ready and working http://oembed-js.appspot.com/?url=[youtube url to embed][&callback=foo] But now where to use it...

In Wordpress blogs, when commenting an article, user can just write/paste youtube url into the comment text, and it will be turned into a video embed. Surprise, not in Blogger. For some blogs this is a bummer. Now with my new, free, javascript/jQuery compatible GAE service oembed-js this became possible in Blogger. Of course we need to hack the blog first a bit. :)


You can decide, whether you want to turn all raw youtube links (watch and playlist links) into embeds, which is the default, or only those, that use [video=youtube_url] tag. You can also change the tag name, if you want, maybe you like youtube or embed more. If you have lazy loading installed in your blog, by default this script will use it, and pages with many videos load much faster. Installation is simple, open template html for editing, find </head> and paste this code before 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. <script type='text/javascript'>
  3. //<![CDATA[
  4. // Embed youtube videos in Blogspot comments by MS-potilas 2012.
  5. // See http://yabtb.blogspot.com/2012/03/embed-youtube-in-blogger-comments.html
  6. //
  7. // if oetag=0, just use video urls like:
  8. //           http://www.youtube.com/watch?v=12345678901
  9. //           surrounded by white space (video tag can be used, too)
  10. // if oetag=1 (for nerdy blogs?), use syntax:
  11. //           [video=http://www.youtube.com/watch?v=12345678901]
  12. // config:
  13. var oetag = 0;           // see above
  14. var oetagname = "video"; // [video=zzz], maybe you like "embed" or "youtube" more?
  15. var oelazy = -1; // -1 detect, 0 = normal, 1 = lazy (needs lazy load hack)
  16. //
  17. function oe_loadscript(filename) {
  18.   var scr=document.createElement('script');
  19.   scr.setAttribute("type","text/javascript");
  20.   scr.setAttribute("src",filename);
  21.   document.getElementsByTagName("head")[0].appendChild(scr);
  22. }
  23. function oe_jumptohash() { // reposition to anchor
  24.   window.scrollTo(0, $("#"+window.location.hash.replace(/^#/, "")).offset().top);
  25. }
  26. var oe_tid;
  27. var oe_elems = {};
  28. //
  29. function oembed_callback(response) {
  30.   var resp = response;
  31.   revurl = resp.url.split("").reverse().join("");
  32.   html = oe_elems[response.callID].html();
  33.   ee = $(resp.html);
  34.   w = parseInt(ee.attr("width"));
  35.   h = parseInt(ee.attr("height"));
  36.   if(oelazy==1) { // convert to lazy load
  37.     src = ee.attr("src");
  38.     src += ((src.indexOf("?")==-1) ? "?" : "&") + "autoplay=1";
  39.     ee.attr("src", src);
  40.     ee.attr("style", "vertical-align:top;");
  41.     htm = $("<div/>").append(ee).html();
  42.     htm = $('<div/>').text(htm).html().replace(/"/g,'&quot;');
  43.     resp.html = '<a href="'+resp.url+'" class="youtube-lazy-link" style="width: '+w+'px; height: '+h+'px; background: #000 url('+resp.thumbnail_url+') center center no-repeat;" onclick="$(this).replaceWith(\''+htm+'\');return false;"><div style="width:'+(w-4)+'px;height:'+(h-4)+'px;" class="youtube-lazy-link-div"></div><div class="youtube-lazy-link-info"><b>'+resp.title+(resp.playlist ? '</b> [playlist]' : '</b>')+'<br /><small>by '+resp.author_name+'</small></div></a>';
  44.   }
  45.   htmlx = html.replace(new RegExp("\\[" + oetagname + "="+resp.url.replace("/", "\\/").replace(".", "\\.").replace("?", "\\?")+"\\]"), " " + resp.url + " ");
  46.   htmlx = htmlx.replace(new RegExp("\\[" + oetagname + "="+resp.url.replace("/", "\\/").replace(".", "\\.").replace("?", "\\?")+" \\]"), " " + resp.url + " ");
  47.   htmlx = htmlx.replace(new RegExp('"'+resp.url.replace("/", "\\/").replace(".", "\\.").replace("?", "\\?")+'"', "g"), '"'+revurl+'"');  // trick to preserve href="url"
  48.   htmlx = htmlx.replace(new RegExp(resp.url.replace("/", "\\/").replace(".", "\\.").replace("?", "\\?")), '<div style="height:'+h+'px;" class="oembed youtube">'+resp.html+'</div>')
  49.   htmlx = htmlx.replace(new RegExp('"'+revurl.replace("/", "\\/").replace(".", "\\.").replace("?", "\\?")+'"', "g"), '"'+resp.url+'"');  // trick to preserve href="url"
  50.   if(html != htmlx) {
  51.     oe_elems[response.callID].html(htmlx);
  52.     if(window.location.hash.replace(/^#/, "").length > 0) {
  53.       if(oe_tid) window.clearTimeout(oe_tid);
  54.       oe_tid = window.setTimeout("oe_jumptohash()", 1000);
  55.     }
  56.   }
  57. }
  58. function oembed_yt(url, width, callID) {
  59.   src = "http://oembed-js.appspot.com/?url=" + encodeURIComponent(url) + "&callback=oembed_callback&callID=" + encodeURIComponent(callID);
  60.   if(width) src = src + "&maxwidth=" + width + "&maxheight=" + width;
  61.   oe_loadscript(src);
  62. }
  63. $(document).ready(function() {
  64.  det=$('<div class="youtube-lazy-link-div" />');
  65.  $("body").append(det);
  66.  if(det.css("position")=="absolute") {
  67.   if(oelazy != 0) oelazy=1;
  68.  }
  69.  else oelazy = 0;
  70.  det.remove();
  71.  window.setTimeout(function() {
  72.   var callID=0;
  73.   $(".comment-content,.comment-body,.comment-body-author").each(function() {
  74.     html = " " + $(this).html() + " ";
  75.     if(oetag)
  76.       matches = html.match(new RegExp("\\["+oetagname+"=(https?:\\/\\/[^\\s<\\/]*youtu\\.*be[^\\]]+)", "g"));
  77.     else
  78.       matches = html.match(/([>\s^]|\[\w+=)(https?:\/\/[^\s<\/]*youtu\.*be[^\s<\]]+)/g);
  79.     if(matches && matches.length) {
  80.       for(var i=0;i<matches.length;i++) {
  81.         url = matches[i].match(/https?:\/\/[^\s<\/]*youtu\.*be[^\s<\]]+/);
  82.         oe_elems[callID] = $(this);
  83.         oembed_yt(url, $(this).width(), callID++);
  84.       }
  85.     }
  86.   });
  87.  }, 500);
  88. });
  89. //]]>
  90. </script>

This script does not work in dynamic views, but it works with Blogger native comments, my threaded comments, and also Blogger threaded comments. Hope you like it. I'll tell you later about the oembed‑js service API (application programming interface), so it is easier for you to develop own applications.

References:
- oEmbed specification
- YouTube API blog: oEmbed support

Update 27th March 2012: Line #74 updated, to handle better the case of only a YouTube url as comment text in new Blogger threaded comments (you do not need to update if you use videotag, or don't use Blogger threaded comments).
See the hack
for this dynamic
views icon: