Yesterday I blogged about using MP3s and ID3 information in a PhoneGap/Cordova application. Today I've taken the initial proof of concept I built in that demo and updated to make use of the Ionic framework. I've also a few other features to make the application a bit more applicable to real world usage. Finally, I've also uploaded it my GitHub repo (along with a copy of the last version) for you to use in your own applications. Before we get into the code, let's take a look at the visual updates.
The first update was the addition of a spinner dialog. I used the spinner from ngCordova.
This will display while the code is parsing the MP3s for their ID3 information. When done, a list is displayed:
Yeah, not very colorful, I really need to add something to the header to make it prettier. But you get the idea. Then when an item is selected, you get a nice Ionic card display:
Now let's break down the code - and remember - you can download everything from the repo I'll link to at the bottom. First - the core app.js for the app:
angular.module('starter', ['ngCordova','ionic', 'starter.controllers', 'starter.services'])
.run(function($ionicPlatform) {
$ionicPlatform.ready(function() {
if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if (window.StatusBar) {
// org.apache.cordova.statusbar required
StatusBar.styleLightContent();
}
});
})
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
$stateProvider
.state('list', {
url: '/',
templateUrl: 'templates/list.html',
controller: 'ListCtrl'
})
.state('list-detail', {
url: '/item/:itemId',
templateUrl: 'templates/detail.html',
controller: 'DetailCtrl'
});
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/');
});
The only thing of note here really is the use of $stateProvider to setup the various states of my app - which in this case is either a list of MP3s or a detail. Now let's look at the controller.
angular.module('starter.controllers', [])
.controller('ListCtrl', function($scope, MP3Service, $cordovaSpinnerDialog) {
console.log('ListCtrl loaded');
document.addEventListener('deviceready', function () {
console.log('begin to get stuff');
$cordovaSpinnerDialog.show("Loading...","", true);
MP3Service.getAll().then(function(results) {
$cordovaSpinnerDialog.hide();
$scope.content = results;
});
});
})
.controller('DetailCtrl', function($scope, $stateParams, MP3Service) {
console.log('DetailCtrl loaded');
$scope.detail = {};
getMediaURL = function(s) {
if(device.platform.toLowerCase() === "android") return "/android_asset/www/" + s;
return s;
}
$scope.play = function() {
console.log('click for '+$scope.detail.url);
MP3Service.play(getMediaURL($scope.detail.url));
};
MP3Service.getOne($stateParams.itemId).then(function(result) {
console.dir(result);
result.description = "Artist: " + result.tags.artist + "<br/>" +
"Album: " + result.tags.album;
$scope.detail = result;
});
});
Ok, so this one is a bit more complex. The first controller, ListCtrl, handles asking a service to return a list of MP3s. It uses the spinner dialog to let the user know "stuff" is going on in the background. Once it has the data, it hides the spinner and the results are displayed. Note the deviceready listener wrapping the call. I forgot this initially and spent about an hour trying to figure out why my app wouldn't run until I did a reload in the console. Dumb, I know, but sometimes when I use Ionic I forget to remember I need deviceready in my controller.
The next controller handles fetching specific information about a MP3 as well as providing a way to play the MP3. I put that in a service as well so I could handle storing the state of the current MP3 being played.
So far so good? Ok, let's take a look at the service. Most of this is from yesterday's post.
angular.module('starter.services', [])
.factory('MP3Service', function($q,$cordovaFile) {
//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;
console.log(mp3Folder);
var deferred = $q.defer();
window.resolveLocalFileSystemURL(mp3Folder, function(dir) {
var reader = dir.createReader();
//read it
reader.readEntries(function(entries) {
console.log("readEntries");
console.dir(entries);
var data = [];
var process = function(index, cb) {
var entry = entries[index];
var name = entry.name;
entry.file(function(file) {
ID3.loadTags(entry.name,function() {
var tags = ID3.getAllTags(name);
//default to filename
var title = entry.name;
if(tags.title) title = tags.title;
//for now - not optimal to include music here, will change later
data.push({name:title, tags:tags, url:mp3Loc+entry.name});
if(index+1 < entries.length) {
process(++index, cb);
} else {
cb(data);
}
},{
dataReader:FileAPIReader(file)
});
});
};
process(0, function(data) {
console.log("Done processing");
console.dir(data);
items = data;
deferred.resolve(items);
});
});
}, function(err) {
deferred.reject(err);
});
return deferred.promise;
}
function getOne(id) {
var deferred = $q.defer();
deferred.resolve(items[id]);
return deferred.promise;
}
var media;
function play(l) {
if(media) { media.stop(); media.release(); }
media = new Media(l,function() {}, function(err) { console.dir(err);});
media.play();
}
return {
getAll:getAll,
getOne:getOne,
play:play
};
});
Ok, there's a lot going on here. First - for this application I decided to ship the MP3s with the application. Now, in a real world app if you were going to do that, you wouldn't bother using an ID3 service. You would simply hard code it. That would be a heck of a lot quicker. But try to imagine an app where MP3s are downloaded after the initial install. This brings up another interesting issue. The area under www is read only, so technically you can't download there. But - and I'm not 100% sure on this - the Media plugin only supports remote URLs and local URLs under www. I could be wrong on that (and I've raised the question on the PhoneGap developer list), but... yeah. I'm not sure how the Media plugin would work with stuff outside of www. For now, I'm going to pretend it isn't an issue.
Another thing I didn't do here is caching. Since the service won't run again when you return to the app home page, I didn't need it, but I'd strongly consider adding a simple caching layer with LocalStorage. I think storing the tags for a path would be simple enough and would take maybe five minutes more work.
And that's pretty much it. You can find the full source here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/mp3reader. Tomorrow I'll have yet another iteration of this demo.
Archived Comments
awesome! whit the ID3, can you get album image?
It is an optional part of ID3 - see the demos on the project - https://github.com/aadsm/Ja.... Also, watch this blog as my next entry in this series will demonstrate a way to do it.
thanks a lot! i'll be waiting for your next entry, i really want to show the album art in my song, instead of use some web service like last.fm to do it
Already posted. :)
thanks! can you paste here the link?
Oh sorry - I misread you. I *did* use last.fm.
Hello! Thanks for this post!
Can You explain what is the class FileAPIReader. Ionic framework based on AngularJS wich have build-in class FileReader with different logic. Or FileAPIReader it's some kind of wrapper for FileReader?
I'm not quite sure what you are asking. I'm using ngCordova which has a wrapper for the Cordova File plugin.
Thanks for Your reply!
Right, I'm using ngCordova too. But there is no FileAPIReader function of class or any.. in ngCordova.
I use same tools as in Your post: Ionic+ngCordova+same id3 lib. But I have android platform added instead IOS. And you example not working for me. Because specified dataReader option in your code (FileAPIReader) for ID3.loadTags function is not existed function.
I'm trying to use native FileReader class as dataReader for ID3 but not successful.
Where do you see "FileAPIReader"? I don't see it in my code.
In article, inside MP3Service.getAll function.
And here - https://github.com/cfjedima...
Oh that's from the ID3 library. You can even see it in the minimized version of the library I used. Ensure you correctly loaded the ID3 library.
Right!) Thanks! I really doesn't found this function!
But anyway I have problems with usage FileAPIReader function. I have not big array of files to process it id3 tags in cycle. On my android device processing of this array in cycle broke application and close it forced. I think it because inside FileAPIReader function creates new instance of FileReader that's in my case occur memory leak.
So You help me to find FileAPIReader function src and I my solution is just pass second parameter in FileAPIReader function to use single instance of FileReader for all files in cycle!
Thank You very much!
Glad to help. Now - you can pay me back if you want. I'm curious about your name. What origin is that? My desire to know is just plain curiosity and if you have *any* reservations about answering a personal question, do not worry!
Aha) That's fun! The answer is really simple - https://en.wikipedia.org/wi...
I'm from Belarus so name written in Cyrillic because it automatically fetched while login via facebook)
Cool, thanks for sharing.
I try to use JavaScript-ID3-Reader in ionicframework
but when run in real android device it's alway throw java.lang.OutOfMemoryError
can you tell me what I do wrong?
entries.forEach(function(entry){
var name = entry.name;
console.log('for each');
entry.file(function(file){
console.log("entry file: "+entry.name);
ID3.loadTags(entry.name, function() {
console.log("load");
var tags = ID3.getAllTags(name);
console.log(tags);
}, {
tags: ["title","artist","album","picture"],
dataReader:FileAPIReader(file)
});
});
});
It means you are running out of memory. Unfortunately there isn't anything I can do about that outside of suggesting you reach out to the library owner to see if they can do something to make the reader a bit more memory friendly.
I think to minimize memory usage in Your case you can create instance of FileReader out from loop and pass it into FileAPIReader function
var reader = new FileReader();
entries.forEach(function(entry){
...
ags: ["title","artist","album","picture"],
dataReader: FileAPIReader(file, reader),
...
}
I think lack of memory happens because into FileAPIReader function every time creates new instance of FileReader if it not passed.
I tried to do this, dear-lord android could be excruciatingly slow takes 2 seconds on desktop took 24 minutes for 200 songs on android :-| although i used `musicmetatags` as it supports passing straight file instance.
Looks good - thanks for sharing that.
Ouch. One thing to remember - at least you can cache the lookup and never do it again.
I am digging deeper, apparently the process was incredible amount of memory forcing the device to freeze up when using the phonegap file API. I am wondering if its the fault of the library or how phonegap implements file API.