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

This post is more than 2 years old.

This week I've done a few blog posts (part one and part two) about MP3 and ID3 parsing in PhoneGap/Cordova applications. Today I'm updating the application again - this time to support album art. Let's look at the results in the simulator first and then I'll walk you through the code.

First - I updated my sample music a bit. My 5 year old loves the Daisy Chainsaw track:

Screen Shot 2015-05-01 at 2.25.27 PM

And here is the detail view - now with album art:

Screen Shot 2015-05-01 at 2.26.14 PM

Screen Shot 2015-05-01 at 2.26.24 PM

Ok, so how did I do this? While ID3 data can actually include album art (see the docs for the JavaScript library I use), it didn't seem like any of my files actually had this data. I made the call that - probably - most files do not. I don't have any scientific data to back this up, but I decided to make use of the API. The API was super easy to use. Like - "Wait, it worked on my first try?" easy. Given that you know an artist and an album, you can use the album.getInfo call to fetch data about the album. This includes multiple different sized images.

Of course, the issue is that each of these API calls is asynchronous. Our MP3 service is already handling the ID3 lookup asynchronously. If you remember, I had to single thread it due to memory issues. But the API calls are jut http calls so running multiple in parallel shouldn't be a problem.


Given that you may have multiple MP3s from the same album, we can improve performance by not requesting the same album cover once we've made one initial request for it.

Ok... so let's take a look at the new services file.

angular.module('', [])

.factory('MP3Service', function($q,$cordovaFile,$http) {
	//root of where my stuff is
	console.log('running service');
	var items = [];

	function getAll() {
		var rootFolder = cordova.file.applicationDirectory;
		var mp3Loc = 'music/';
		//where the music is
		var mp3Folder = rootFolder + 'www/' + mp3Loc;

		var deferred = $q.defer();

		window.resolveLocalFileSystemURL(mp3Folder, function(dir) {
			var reader = dir.createReader();
			//read it
			reader.readEntries(function(entries) {

					var data = [];

					var process = function(index, cb) {
						var entry = entries[index];
						var name =;
						entry.file(function(file) {

							ID3.loadTags(,function() {
								var tags = ID3.getAllTags(name);
								//default to filename
								var title =;
								if(tags.title) title = tags.title;
								//for now - not optimal to include music here, will change later
								data.push({name:title, tags:tags,});
								if(index+1 < entries.length) {
									process(++index, cb);
								} else {



					process(0, function(data) {
						console.log("Done processing");
						items = data;
						// New logic - now we get album art
						var defs = [];
						//remember artist + album
						var albums = {};
						for(var i=0;i<items.length;i++) {
							var album = items[i].tags.album;
							var artist = items[i].tags.artist;
							console.log("album="+album+" artist="+artist);
							if(albums[album+" "+artist]) {
								console.log("get from album cache");
								var def =  $q.defer();
								def.resolve({cache:album+" "+artist});
							} else {
								albums[album+" "+artist] = 1;
						$q.all(defs).then(function(res) {
							console.log("in the q all");
							for(var i=0;i<res.length;i++) {
								console.log(i, res[i]);
								//if we marked it as 'from cache', check cache
								if(res[i].cache) {
									console.log('setting from cache '+albums[res[i].cache])
									items[i].image = albums[res[i].cache];
								} else {
									var result = res[i].data;
									//potential match at result.album.image
									if(result.album && result.album.image) {
										items[i].image = result.album.image[3]["#text"];
									} else {
										items[i].image = "";
									albums[items[i].tags.album+" "+items[i].tags.artist] = items[i].image;


		}, function(err) {

		return deferred.promise;

	function getOne(id) {
		var deferred = $q.defer();

		return deferred.promise;

	var media;
	function play(l) {
		if(media) { media.stop(); media.release(); }
		media = new Media(l,function() {}, function(err) { console.dir(err);});;
	return {

Normally I trim out the console.log messages as noise, but I kept them in due to the complexity of this service. The important bits begin in the process(0, function(data)) callback. The general idea is this:

  1. Loop over all the MP3s.
  2. Get the album and artist. (This needs to be improved to see if the tags exist.)
  3. Check the albums object to see if we have already fetched it. Note - at this moment, the cache object is just a flag. The initial request isn't actually done yet. But we want to know that we've already done a request for that album.
  4. If we aren't, hit Note that the API key should be stripped out and put into a constants block. I've temporarily changed the key above to a non-legit value.
  5. We've created an array of deferred objects. These represent the async operations (and yes, some aren't async, which is ok, we can still use deferreds for them). I can then use $q.all to say, "Do this crap when ALL of them are done."
  6. In that handler, I see if I've marked this as an item that should use the cache. In theory, this will never be run before an item that used the cache, so I check the albums object, which now has a real value in it, and use that.
  7. If this isn't a "use the cache item", I fetch out the image from the result data from and store the cache.

And that's it. I then just updated the view to make use of the image. I've updated the GitHub repo with this version:

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA

Archived Comments

Comment 1 by тимофей чернявский posted on 11/13/2015 at 11:28 AM


Thank you for this articles!

looks like you have a lot of experience working with file api. I have a new problem related with ID3 tags reading.

As You can see on attached screenshot - some file error occurs somewhere in promise callback..or somewhere else...I cant understand why. Maybe You know why this error happen usually?

Explanation of error:
"The operation cannot be performed on the current state of the interface object. For example, the state that was cached in an interface object has changed since it was last read from disk."

Comment 2 (In reply to #1) by Raymond Camden posted on 11/13/2015 at 2:28 PM

It appears as if an error in the promise isn't being handled. Have you tried adding an error handler to the code working with the promise?