How-to: Music Platform App

DZone 's Guide to

How-to: Music Platform App

· Web Dev Zone ·
Free Resource


Jerome Denanot is a senior J2EE architect since 2000, that worked on a Cappuccino to J2EE bridge from 2009 to 2010 (CP2JavaWS). This solution also brings a mda approach.

Project idea

Being a composer and after having tried common platforms through aggregators, with same result as most artists (that is with no visibility), I thought about providing a new platform, open source and free. I had also ideas about a fair ranking system, that would address ranking dynamics problems introduced since AppStore. The web application had to be fun and look great. It also had to be fast (lite architecture) to allow deployment at minimal cost (as it is a free project) and scallability.

Porting to php

As most hosting providers don't allow Java on server side, and as php brings less overhead, I started porting the server part of the bridge to php, that is transparent call of a remote php service from the Cappuccino service proxy.

For this application there were just anonymous php objects for the model, then the php controller only manages array and anonymous objects (correspond to Cappucccino dictionary) for now, that is ok and fast (the Java part manages unlimited nested Java objects graphs, with cycle/references).

Digest authentication was also included (as found in the Java part). It allows the same transparent (based on Direct2CP's proxy class) call of a remote service :

var endPoint = [DCEndPoint createForURL:"/controller.php"];
var remoteService = [endPoint proxyForRemoteService:@"SomeService" delegate:self]; // php service class name (using Java we would use the service interface)
[remoteService method1:"arg1StringValue" arg2:2 arg3:new Date() delegateRespHandler:@selector(successMethod:) delegateFailHandler:@selector(failMethod:)];

Enhancing the mda approach

The original CP2JavaWS bridge still provided creation of master/detail views, with live remote data fetching and full management of CRUD cycle (using a generic remote service - based on Hibernate - or a custom one), using a single line of code (name of the Cappuccino model class).

Since Atlas project was cancelled, there was a need to create forms from code (as nib2cib tool requires Xcode and then a mac), using fast syntax and management of validation errors. The new Direct2CP framework then brings creation of form fields and dynamic validation (js or remote service), using a single line of code. Moreover forms are integrated with the full CRUD cycle management (can be used as a detail view).

To push the solution further I also added best practices to allow nested and composite views, while keeping automatic management of master/detail and CRUD cycle. This is shown in the Release tab : the main view is a Direct2CP master/detail view, whose detail view is a composite view. This view contains both a form view (release details) and another master/detail view, whose detail view (form view) displays informations from the selected song!


- AppController's createReleasesTab (brown) :

var releaseMasterDetailView = [DCMasterDetailView createDeferredForEndPoint:endPoint1 idAttName:"id" masterAttributes:["id", "title", "releasedate", "purchases", "status"] labels:["", "Release", "Date", "Sales", "St"] sizes:[0, 120, 80, 40, 30] criterias:nil rect:CPRectInset(aRect, 0, 0) hasOffset:YES detailView:[[ReleaseDetailView alloc] init] layout:MasterDetailView_horizontal_layout font:[CPFont boldSystemFontOfSize:12.0] javaServiceInterfaceName:"ReleaseService" readMethod:"readReleasesForUser" sizeMethod:"releasesCountForUser" readUniqueMethodeName:"readRelease" updateMethodName:"updateRelease" insertMethodName:"saveRelease" deleteMethodName:"deleteRelease"];

- ReleaseDetailView (green) :

[releaseDetailForm addFieldWithLabel:@"Title" key:@"title" validation:FormView_VALIDATION_REQUIRED size:140 action:nil];
[releaseDetailForm addMultiLineFieldWithLabel:@"Desc." key:@"description" validation:FormView_VALIDATION_REQUIRED size:390 height:100 action:nil];
[releaseDetailForm addListWithLabel:@"Type" key:@"type" validation:FormView_VALIDATION_REQUIRED codes:['A', 'B', 'O', 'P'] labels:["Ambient", "Ballad", "Orchestral", "Pop/Rock"] size:120];
[releaseDetailForm nextRow];
[releaseDetailForm addLabel:@"Picture (drop)" originX:480 originY:20];
var imageField = [releaseDetailForm addImage:@"Resources/upload.png" key:@"picturepath" width:120 height:120];
imageDropUploadController = [[DropUploadController alloc] initWithDropView:imageField uploadUrl:@"upload.php" maxSize:100000 fileTypes:["gif", "png", "jpg", "jpeg"] delegate:self];
[releaseDetailForm addButton:@"Save" width:80 height:24.0 action:@selector(submit) cornerRelative:YES];
[releaseDetailForm addButton:@"New" width:80 height:24.0 action:@selector(manageNew) cornerRelative:YES];
[releaseDetailForm addButton:@"Delete" width:80 height:24.0 action:@selector(delete) cornerRelative:YES];
[self addSubview:releaseDetailForm];
songsMasterDetailView = [DCMasterDetailView createForEndPoint:endPoint idAttName:"s.id" masterAttributes:["s.id", "s.title", "s.status"] labels:["", "Song", "St"] sizes:[0, 120, 20] criterias:nil rect:songsMasterDetailRect hasOffset:NO detailView:[[SongDetailView alloc] init] layout:MasterDetailView_horizontal_layout font:[CPFont boldSystemFontOfSize:12.0] javaServiceInterfaceName:"SongService" readMethod:"readSongsForUser" sizeMethod:"songsCountForUser" readUniqueMethodeName:"readSong" updateMethodName:"updateSong" insertMethodName:"insertSong" deleteMethodName:"deleteSong"];
[self addSubview:songsMasterDetailView];

SongDetailView (yellow) :

[songDetailForm addFieldWithLabel:@"Title" key:@"title" validation:FormView_VALIDATION_REQUIRED size:130 action:nil];
var urlField = [songDetailForm addFieldWithLabel:@"Url" key:@"path" validation:FormView_VALIDATION_REQUIRED size:130 action:nil];
urlDropUploadController = [[DropUploadController alloc] initWithDropView:urlField uploadUrl:@"upload.php" maxSize:25000000 fileTypes:["zip", "7z"] delegate:self];
var sampleUrlField = [songDetailForm addFieldWithLabel:@"Sample url" key:@"samplepath" validation:FormView_VALIDATION_REQUIRED size:130 action:nil];
sampleUrlDropUploadController = [[DropUploadController alloc] initWithDropView:sampleUrlField uploadUrl:@"upload.php" maxSize:10000000 fileTypes:["m4a", "mp3", "wav"] delegate:self];
var altSampleUrlField = [songDetailForm addFieldWithLabel:@"Alt. sample" key:@"altsamplepath" size:130 action:nil tooltip:@"Alt. ogg sample for Firefox"];
altSampleUrlDropUploadController = [[DropUploadController alloc] initWithDropView:altSampleUrlField uploadUrl:@"upload.php" maxSize:10000000 fileTypes:["ogg"] delegate:self];
[songDetailForm setPopoverMessage:@"Also set alt. path to an ogg file for Firefox (or set a wav file)" forKey:@"samplepath" behaviour:CPPopoverBehaviorTransient];
[songDetailForm addSecureFieldWithLabel:@"Password" key:@"password" validation:FormView_VALIDATION_REQUIRED encryption:FormView_AES_ENCRYPTION size:120 action:nil];
[songDetailForm addFieldWithLabel:@"Length (s)" key:@"length" size:100 action:nil];
[songDetailForm addListWithLabel:@"Format" key:@"format" validation:FormView_VALIDATION_REQUIRED codes:['A', 'H', 'F', 'L'] labels:["Apple Lossless", "Apple Lossless 24 bits", "Flac", "Flac 24 bits"] size:120];
[songDetailForm addDatePickerWithLabel:@"Composed on" key:@"compDate"];
[songDetailForm addButton:@"Save" width:80 height:24.0 action:@selector(submit) cornerRelative:YES];
[songDetailForm addButton:@"New" width:80 height:24.0 action:@selector(manageNew) cornerRelative:YES];
[songDetailForm addButton:@"Delete" width:80 height:24.0 action:@selector(delete) cornerRelative:YES];
[self addSubview:songDetailForm];


The player

As all would be centered around the audio player, it had to be really impressive, easy reachable, and bring some dynamic to the whole application. It also should be well integrated to Cappuccino and not bring dependencies.

After having tried most HTML5 audio based solutions, I was impressed by the Zen player, that uses CSS3. However it required jquery and jplayer. As Cappuccino still provides a CPSound component (wraps an HTML5 audio tag) the Cappuccino Zend player didn't need jplayer. Access to css properties without using jquery however required some js utility code (easily found and tweaked).
The CPZenPlayer then wraps both a CPSound and DOMElement corresponding to the original Zen player HTML divs.The original css file was slightly modified to fix rendering problem in Firefox. The player keeps playing when swtiching tabs, and also manages custom audio files for Firefox (ogg - Firefox is expected to support mp3 and mp4 soon).


EMZenView.j :
- (id)initWithFrame:(CGRect)aFrame {
_zenDiv = document.createElement("div");
_zenDiv.id = "zen";
_zenDiv.style.margin = "30px 30px";
_playerSpan = document.createElement("span");
FM.addClass(_playerSpan, "player"); // FM is the css utility class that allows adding and remowing css class to elements, and setting css3 rotate values
_circleSpan = document.createElement("span");
FM.addClass(_circleSpan, "circle");
_progressSpan = document.createElement("span");
FM.addClass(_progressSpan, "progress");
_bufferSpan = document.createElement("span");
FM.addClass(_bufferSpan, "buffer");
_dragSpan = document.createElement("span");
FM.addClass(_dragSpan, "drag");
_buttonDiv = document.createElement("div");
FM.addClass(_buttonDiv, "button");
_playIcon = document.createElement("span");
FM.addClass(_playIcon, "icon play");
_pauseIcon = document.createElement("span");
FM.addClass(_pauseIcon, "icon pause");
audioController = [[CPSound alloc] init];
audioController._audioTag.preload = "auto"; // to allow buffering
[audioController setDelegate:self];
[audioController setVolume:1.0];
audioController._audioTag.addEventListener("progress", function(event)
[self _progress:event];
audioController._audioTag.addEventListener("timeupdate", function(event)
[self _timeupdate:event];
- (void)_timeupdate:(CPEvent)anEvent {
var posPercent = (audioController._audioTag.currentTime / audioController._audioTag.duration) * 100;
[self displayProgress:posPercent];

- (void) displayProgress:(CPInteger)aProgPos {

var degs = aProgPos * 3.6+"deg";
FM.rotate(_progressSpan, degs); // FM.rotate in css-class-function used -webkit-transform only


Other dynamic elements

Switching tabs uses Cappuccino implementation of Core Animation (based on Canvas for now) to animate views (with live resizing of its subviews - faster on Safari). Also the application makes many use of the recent CPPopover component (pops a box message using CSS3 around form fields that need focus or to display reasons of a song reject from a community member/validator).

Help and terms of service panels use the great Cappuccino implementation of Apple's sheets (animated sheets that pop down from the menu panel). There is also dynamic from display of upload progress (see below).


In order not to require management, payment is uncentralized, using Paypal's secure encrypted buttons (ensures that payment informations can't be modified) and easy configuration (Paypal grant API to allow encryption on the behalf of sellers, using the site's certificate - that is without requiring artists to upload their own certificate).

Uncentralized storage and HTML5 upload

Files management also had to be uncentralized, in order for the site infrastructure to only manage lite requests (that is services calls and json results). Moreover there were many free cloud services (Dropox, Sugarsync, and now GoogleDrive). It would however be great to integrate directly to Dropbox for example.

As Dropbox does not provide a js based upload framework (only used from its own site), and as to keep consumer key private, the application uses the php Dropbox framework to grant access (it is based on the common OAuth protocol, also used by others like Twitter). There data have then to be first uploaded to the server (temporary), and then uploaded from the server to Dropbox.

 Cappuccino upload components weren't however reliable (didn't work with Firefox and experienced problems), then I created a new upload component (based on open source code) that would manage both Safari default upload and Firefox use of recent HTML5 FileReader and BlobReader. It could also be enhanced as we can find custom js implementations of FileReader and BlobReader that may be used for Safari (there was problems with upload since Safari 5.1 - used to manage FileReader before).

The upload component is provided as a form component (attached to a text form field or image view field) and manages display of progress. For the second step (upload from server to Dropbox), that can't be polled (Dropbox does not allow chunk requests), it displays a spinning circle (however it is ok as it is way faster - a fews seconds - than upload from user connection to server).

Performance and user experience

The user experience is the same using Safari or Firefox. Safari is faster at drawing (tabs switchs, refresh of tables and forms), however Firefox is more reliable for uploading releases images (song files are uploaded fine however by Safari).

The application also now uses Cappuccino's press tool to compress .j files (first compiled in javascript, then compressed). the whole application code (except Cappuccino framework) is now a single 350 KB .sj file. It also uses the new deferred mode for the master detail views (added in Direct2CP version 1.1) : that is only one request is sent initially (for the main store tab), no requests are sent for empty table views from other tabs. In details, with the deferred parameter to true, the master view's delegate's numberOfRows methods returns 0 immediately (without calling the synchronously required remote method). It is reset to false when calling setCriteria, setSortDescriptorsStr or reset. Performance is now related to the hosting provider.


Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}