Welcome to the fourth and final entry in my series on using an ID3 reader for MP3s in a Cordova application. If you missed the initial entries (and I highly recommend reading these in order), they are:

  1. Working with MP3s, ID3, and PhoneGap/Cordova
  2. Working with MP3s, ID3, and PhoneGap/Cordova (2)
  3. Working with MP3s, ID3, and PhoneGap/Cordova (3)

In this series, I described how we could use a JavaScript library and the Cordova File plugin to get ID3 info from MP3 files. In the last entry I made use of the last.fm API to fetch album covers. Their API was simple to use, but you may have noticed something - my API key was embedded in the code:

defs.push($http.get("http://ws.audioscrobbler.com/2.0/?method=album.getinfo&artist=" + encodeURI(artist) + "&album=" + encodeURI(album) + "&api_key=5poo53&format=json"));

If my app was out in the world, anyone with two minutes to spare could fire up remote debugging and take a look at the code to find the API key. This is where something like MobileFirst can save the day. Back on April 8th (which was my birthday by the way, do I look older?) I blogged about using HTTP Adapters with MobileFirst.

The basic idea is simple:

  1. You define your adapter at the command line. For me, I called mine lastfm.

  2. You edit the adapter XML as needed. For me, I modified the domain for my service and specified a procedure name:

<?xml version="1.0" encoding="UTF-8"?>
<!--
    Licensed Materials - Property of IBM
    5725-I43 (C) Copyright IBM Corp. 2011, 2013. All Rights Reserved.
    US Government Users Restricted Rights - Use, duplication or
    disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
-->
<wl:adapter name="lastfm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:wl="http://www.ibm.com/mfp/integration"
	xmlns:http="http://www.ibm.com/mfp/integration/http">

	<displayName>lastfm</displayName>
	<description>lastfm</description>
	<connectivity>
		<connectionPolicy xsi:type="http:HTTPConnectionPolicyType">
			<protocol>http</protocol>
			<domain>ws.audioscrobbler.com</domain>
			<port>80</port>	
			<connectionTimeoutInMilliseconds>30000</connectionTimeoutInMilliseconds>
			<socketTimeoutInMilliseconds>30000</socketTimeoutInMilliseconds>
			<maxConcurrentConnectionsPerNode>50</maxConcurrentConnectionsPerNode>
			<!-- Following properties used by adapter's key manager for choosing specific certificate from key store  
			<sslCertificateAlias></sslCertificateAlias> 
			<sslCertificatePassword></sslCertificatePassword>
			-->		
		</connectionPolicy>
	</connectivity>

	<procedure name="getAlbumCover"/>
	
</wl:adapter>

In the code snippet above, ws.audioscrobbler.com is the last.fm API domain and getAlbumCover is the procedure name.

  1. You then write your implementation code. Since the API is simple - pass in an album and artist - my code is simple.
function getAlbumCover(artist,album) {

	var input = {
	    method : 'get',
	    returnedContentType : 'json',
	    path : "2.0/?method=album.getinfo&artist="+encodeURI(artist)+"&album="+encodeURI(album)+"&api_key=poo&format=json"
	};

	WL.Logger.info("getDetail, requesting artist "+artist+" and album "+album);
	WL.Logger.info(input.path);
	
	var result = WL.Server.invokeHttp(input);
	
	if(!result.album) {
		return {"error":result.message};
	}
	WL.Logger.info("got this image "+result.album.image[3]["#text"]);
	return {"img":result.album.image[3]["#text"]};
	
}

My function takes the arguments and generates a URL pointing to the API. Of special note - notice how we return only the image. Not only have we abstracted out the service from the client, allowing us to switch to a new provider if we need to, but we've also dramatically reduced the network packet sent to the client. How much so? Here is the full result of a 'regular' last.fm API call:

{
  "statusCode": 200,
  "isSuccessful": true,
  "album": {
    "id": "2634331",
    "listeners": "2453",
    "mbid": "",
    "toptags": {
      "tag": {
        "name": "ahmet",
        "url": "http://www.last.fm/tag/ahmet"
      }
    },
    "name": "Wish",
    "image": [
      {
        "#text": "http://images.amazon.com/images/P/B0000263H7.01._SCMZZZZZZZ_.jpg",
        "size": "small"
      },
      {
        "#text": "http://images.amazon.com/images/P/B0000263H7.01._SCMZZZZZZZ_.jpg",
        "size": "medium"
      },
      {
        "#text": "http://images.amazon.com/images/P/B0000263H7.01._SCMZZZZZZZ_.jpg",
        "size": "large"
      },
      {
        "#text": "http://images.amazon.com/images/P/B0000263H7.01._SCMZZZZZZZ_.jpg",
        "size": "extralarge"
      },
      {
        "#text": "http://images.amazon.com/images/P/B0000263H7.01._SCMZZZZZZZ_.jpg",
        "size": "mega"
      }
    ],
    "releasedate": "    ",
    "playcount": "28392",
    "artist": "Cure",
    "tracks": "\n            ",
    "url": "http://www.last.fm/music/+noredirect/Cure/Wish"
  },
  "statusReason": "OK",
  "responseHeaders": {
    "X-Web-Node": "www173",
    "Date": "Wed, 06 May 2015 19:44:37 GMT",
    "Access-Control-Allow-Origin": "*",
    "Vary": "Accept-Encoding",
    "Expires": "Thu, 07 May 2015 19:44:37 GMT",
    "Access-Control-Max-Age": "86400",
    "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
    "Connection": "close",
    "Content-Type": "application/json; charset=utf-8;",
    "Server": "Apache/2.2.22 (Unix)",
    "Cache-Control": "max-age=86400"
  },
  "responseTime": 590,
  "totalTime": 591
}>

And here is what is returned now:

{
  "isSuccessful": true,
  "image": "http://images.amazon.com/images/P/B0000263H7.01._SCMZZZZZZZ_.jpg"
}

That's a rather significant reduction.

  1. The final piece is to update the client side code. I had to make two changes. First, instead of using $http, I used WLResourceRequest. You can see a good doc on this here, but this is how my new code looks:
var request = new WLResourceRequest('/adapters/lastfm/getAlbumCover', WLResourceRequest.GET);
request.setQueryParameter('params',[artist,album]);
defs.push(request.send());

WLResourceRequest returns a promise, so it was pretty much a two second mod. setQueryParameter threw me for a loop though. If you try to use individual parameters, like so:

request.setQueryParameter('artist', artist);
request.setQueryParameter('album', album);

Then it will not work. The doc I linked to above makes this clear, but it was easy to miss. The last thing I tweaked was the result handling code:

if(result.responseJSON.img) {
    items[i].image = result.responseJSON.img;

As I said above, now my API use is both agnostic, and a bit more secure. I'm not saying it is 100% secure - in my sample app I'm not using login so anyone could sniff the network request and try to hack it, but it's a heck of a lot more locked down then it was before.

p.s. I had to make one more small tweak, and I plan on calling this out in it's own blog post. When using the file system and assets under www, MobileFirst takes your www assets from common and puts them in www/default. I kept getting "File Not Found" errors trying to parse my MP3s and that explained why. I'll discuss this more in a future blog post.