").append(b.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},b.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){b.fn[t]=function(e){return this.on(t,e)}}),b.each(["get","post"],function(e,n){b[n]=function(e,r,i,o){return b.isFunction(r)&&(o=o||i,i=r,r=t),b.ajax({url:e,type:n,dataType:o,data:r,success:i})}}),b.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Nn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":b.parseJSON,"text xml":b.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Mn(Mn(e,b.ajaxSettings),t):Mn(b.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,u,l,c,p=b.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?b(f):b.event,h=b.Deferred(),g=b.Callbacks("once memory"),m=p.statusCode||{},y={},v={},x=0,T="canceled",N={readyState:0,getResponseHeader:function(e){var t;if(2===x){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===x?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return x||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return x||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>x)for(t in e)m[t]=[m[t],e[t]];else N.always(e[N.status]);return this},abort:function(e){var t=e||T;return l&&l.abort(t),k(0,t),this}};if(h.promise(N).complete=g.add,N.success=N.done,N.error=N.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=b.trim(p.dataType||"*").toLowerCase().match(w)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?80:443))==(mn[3]||("http:"===mn[1]?80:443)))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=b.param(p.data,p.traditional)),qn(An,p,n,N),2===x)return N;u=p.global,u&&0===b.active++&&b.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Cn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(b.lastModified[o]&&N.setRequestHeader("If-Modified-Since",b.lastModified[o]),b.etag[o]&&N.setRequestHeader("If-None-Match",b.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&N.setRequestHeader("Content-Type",p.contentType),N.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)N.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,N,p)===!1||2===x))return N.abort();T="abort";for(i in{success:1,error:1,complete:1})N[i](p[i]);if(l=qn(jn,p,n,N)){N.readyState=1,u&&d.trigger("ajaxSend",[N,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){N.abort("timeout")},p.timeout));try{x=1,l.send(y,k)}catch(C){if(!(2>x))throw C;k(-1,C)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,C=n;2!==x&&(x=2,s&&clearTimeout(s),l=t,a=i||"",N.readyState=e>0?4:0,r&&(w=_n(p,N,r)),e>=200&&300>e||304===e?(p.ifModified&&(T=N.getResponseHeader("Last-Modified"),T&&(b.lastModified[o]=T),T=N.getResponseHeader("etag"),T&&(b.etag[o]=T)),204===e?(c=!0,C="nocontent"):304===e?(c=!0,C="notmodified"):(c=Fn(p,w),C=c.state,y=c.data,v=c.error,c=!v)):(v=C,(e||!C)&&(C="error",0>e&&(e=0))),N.status=e,N.statusText=(n||C)+"",c?h.resolveWith(f,[y,C,N]):h.rejectWith(f,[N,C,v]),N.statusCode(m),m=t,u&&d.trigger(c?"ajaxSuccess":"ajaxError",[N,p,c?y:v]),g.fireWith(f,[N,C]),u&&(d.trigger("ajaxComplete",[N,p]),--b.active||b.event.trigger("ajaxStop")))}return N},getScript:function(e,n){return b.get(e,t,n,"script")},getJSON:function(e,t,n){return b.get(e,t,n,"json")}});function _n(e,n,r){var i,o,a,s,u=e.contents,l=e.dataTypes,c=e.responseFields;for(s in c)s in r&&(n[c[s]]=r[s]);while("*"===l[0])l.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in u)if(u[s]&&u[s].test(o)){l.unshift(s);break}if(l[0]in r)a=l[0];else{for(s in r){if(!l[0]||e.converters[s+" "+l[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==l[0]&&l.unshift(a),r[a]):t}function Fn(e,t){var n,r,i,o,a={},s=0,u=e.dataTypes.slice(),l=u[0];if(e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u[1])for(i in e.converters)a[i.toLowerCase()]=e.converters[i];for(;r=u[++s];)if("*"!==r){if("*"!==l&&l!==r){if(i=a[l+" "+r]||a["* "+r],!i)for(n in a)if(o=n.split(" "),o[1]===r&&(i=a[l+" "+o[0]]||a["* "+o[0]])){i===!0?i=a[n]:a[n]!==!0&&(r=o[0],u.splice(s--,0,r));break}if(i!==!0)if(i&&e["throws"])t=i(t);else try{t=i(t)}catch(c){return{state:"parsererror",error:i?c:"No conversion from "+l+" to "+r}}}l=r}return{state:"success",data:t}}b.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return b.globalEval(e),e}}}),b.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),b.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=o.head||b("head")[0]||o.documentElement;return{send:function(t,i){n=o.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var On=[],Bn=/(=)\?(?=&|$)|\?\?/;b.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=On.pop()||b.expando+"_"+vn++;return this[e]=!0,e}}),b.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,u=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return u||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=b.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,u?n[u]=n[u].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||b.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,On.push(o)),s&&b.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}b.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=b.ajaxSettings.xhr(),b.support.cors=!!Rn&&"withCredentials"in Rn,Rn=b.support.ajax=!!Rn,Rn&&b.ajaxTransport(function(n){if(!n.crossDomain||b.support.cors){var r;return{send:function(i,o){var a,s,u=n.xhr();if(n.username?u.open(n.type,n.url,n.async,n.username,n.password):u.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)u[s]=n.xhrFields[s];n.mimeType&&u.overrideMimeType&&u.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)u.setRequestHeader(s,i[s])}catch(l){}u.send(n.hasContent&&n.data||null),r=function(e,i){var s,l,c,p;try{if(r&&(i||4===u.readyState))if(r=t,a&&(u.onreadystatechange=b.noop,$n&&delete Pn[a]),i)4!==u.readyState&&u.abort();else{p={},s=u.status,l=u.getAllResponseHeaders(),"string"==typeof u.responseText&&(p.text=u.responseText);try{c=u.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,l)},n.async?4===u.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},b(e).unload($n)),Pn[a]=r),u.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+x+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n,r,i=this.createTween(e,t),o=Yn.exec(t),a=i.cur(),s=+a||0,u=1,l=20;if(o){if(n=+o[2],r=o[3]||(b.cssNumber[e]?"":"px"),"px"!==r&&s){s=b.css(i.elem,e,!0)||n||1;do u=u||".5",s/=u,b.style(i.elem,e,s+r);while(u!==(u=i.cur()/a)&&1!==u&&--l)}i.unit=r,i.start=s,i.end=o[1]?s+(o[1]+1)*n:n}return i}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=b.now()}function Zn(e,t){b.each(t,function(t,n){var r=(Qn[t]||[]).concat(Qn["*"]),i=0,o=r.length;for(;o>i;i++)if(r[i].call(e,t,n))return})}function er(e,t,n){var r,i,o=0,a=Gn.length,s=b.Deferred().always(function(){delete u.elem}),u=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,l.startTime+l.duration-t),r=n/l.duration||0,o=1-r,a=0,u=l.tweens.length;for(;u>a;a++)l.tweens[a].run(o);return s.notifyWith(e,[l,o,n]),1>o&&u?n:(s.resolveWith(e,[l]),!1)},l=s.promise({elem:e,props:b.extend({},t),opts:b.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=b.Tween(e,l.opts,t,n,l.opts.specialEasing[t]||l.opts.easing);return l.tweens.push(r),r},stop:function(t){var n=0,r=t?l.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)l.tweens[n].run(1);return t?s.resolveWith(e,[l,t]):s.rejectWith(e,[l,t]),this}}),c=l.props;for(tr(c,l.opts.specialEasing);a>o;o++)if(r=Gn[o].call(l,e,c,l.opts))return r;return Zn(l,c),b.isFunction(l.opts.start)&&l.opts.start.call(e,l),b.fx.timer(b.extend(u,{elem:e,anim:l,queue:l.opts.queue})),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always)}function tr(e,t){var n,r,i,o,a;for(i in e)if(r=b.camelCase(i),o=t[r],n=e[i],b.isArray(n)&&(o=n[1],n=e[i]=n[0]),i!==r&&(e[r]=n,delete e[i]),a=b.cssHooks[r],a&&"expand"in a){n=a.expand(n),delete e[r];for(i in n)i in e||(e[i]=n[i],t[i]=o)}else t[r]=o}b.Animation=b.extend(er,{tweener:function(e,t){b.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,u,l,c,p,f=this,d=e.style,h={},g=[],m=e.nodeType&&nn(e);n.queue||(c=b._queueHooks(e,"fx"),null==c.unqueued&&(c.unqueued=0,p=c.empty.fire,c.empty.fire=function(){c.unqueued||p()}),c.unqueued++,f.always(function(){f.always(function(){c.unqueued--,b.queue(e,"fx").length||c.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[d.overflow,d.overflowX,d.overflowY],"inline"===b.css(e,"display")&&"none"===b.css(e,"float")&&(b.support.inlineBlockNeedsLayout&&"inline"!==un(e.nodeName)?d.zoom=1:d.display="inline-block")),n.overflow&&(d.overflow="hidden",b.support.shrinkWrapBlocks||f.always(function(){d.overflow=n.overflow[0],d.overflowX=n.overflow[1],d.overflowY=n.overflow[2]}));for(i in t)if(a=t[i],Vn.exec(a)){if(delete t[i],u=u||"toggle"===a,a===(m?"hide":"show"))continue;g.push(i)}if(o=g.length){s=b._data(e,"fxshow")||b._data(e,"fxshow",{}),"hidden"in s&&(m=s.hidden),u&&(s.hidden=!m),m?b(e).show():f.done(function(){b(e).hide()}),f.done(function(){var t;b._removeData(e,"fxshow");for(t in h)b.style(e,t,h[t])});for(i=0;o>i;i++)r=g[i],l=f.createTween(r,m?s[r]:0),h[r]=s[r]||b.style(e,r),r in s||(s[r]=l.start,m&&(l.end=l.start,l.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}b.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(b.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?b.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=b.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){b.fx.step[e.prop]?b.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[b.cssProps[e.prop]]||b.cssHooks[e.prop])?b.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},b.each(["toggle","show","hide"],function(e,t){var n=b.fn[t];b.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),b.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=b.isEmptyObject(e),o=b.speed(t,n,r),a=function(){var t=er(this,b.extend({},e),o);a.finish=function(){t.stop(!0)},(i||b._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=b.timers,a=b._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&b.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=b._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=b.timers,a=r?r.length:0;for(n.finish=!0,b.queue(this,e,[]),i&&i.cur&&i.cur.finish&&i.cur.finish.call(this),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}b.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){b.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),b.speed=function(e,t,n){var r=e&&"object"==typeof e?b.extend({},e):{complete:n||!n&&t||b.isFunction(e)&&e,duration:e,easing:n&&t||t&&!b.isFunction(t)&&t};return r.duration=b.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in b.fx.speeds?b.fx.speeds[r.duration]:b.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){b.isFunction(r.old)&&r.old.call(this),r.queue&&b.dequeue(this,r.queue)},r},b.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},b.timers=[],b.fx=rr.prototype.init,b.fx.tick=function(){var e,n=b.timers,r=0;for(Xn=b.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||b.fx.stop(),Xn=t},b.fx.timer=function(e){e()&&b.timers.push(e)&&b.fx.start()},b.fx.interval=13,b.fx.start=function(){Un||(Un=setInterval(b.fx.tick,b.fx.interval))},b.fx.stop=function(){clearInterval(Un),Un=null},b.fx.speeds={slow:600,fast:200,_default:400},b.fx.step={},b.expr&&b.expr.filters&&(b.expr.filters.animated=function(e){return b.grep(b.timers,function(t){return e===t.elem}).length}),b.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){b.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,b.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},b.offset={setOffset:function(e,t,n){var r=b.css(e,"position");"static"===r&&(e.style.position="relative");var i=b(e),o=i.offset(),a=b.css(e,"top"),s=b.css(e,"left"),u=("absolute"===r||"fixed"===r)&&b.inArray("auto",[a,s])>-1,l={},c={},p,f;u?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),b.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(l.top=t.top-o.top+p),null!=t.left&&(l.left=t.left-o.left+f),"using"in t?t.using.call(e,l):i.css(l)}},b.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===b.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),b.nodeName(e[0],"html")||(n=e.offset()),n.top+=b.css(e[0],"borderTopWidth",!0),n.left+=b.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-b.css(r,"marginTop",!0),left:t.left-n.left-b.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||o.documentElement;while(e&&!b.nodeName(e,"html")&&"static"===b.css(e,"position"))e=e.offsetParent;return e||o.documentElement})}}),b.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);b.fn[e]=function(i){return b.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?b(a).scrollLeft():o,r?o:b(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return b.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}b.each({Height:"height",Width:"width"},function(e,n){b.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){b.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return b.access(this,function(n,r,i){var o;return b.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?b.css(n,r,s):b.style(n,r,i,s)},n,a?i:t,a,null)}})}),e.jQuery=e.$=b,"function"==typeof define&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return b})})(window);
\ No newline at end of file
diff --git a/js/underscore.js b/js/underscore.js
new file mode 100644
index 0000000..7ed6e52
--- /dev/null
+++ b/js/underscore.js
@@ -0,0 +1 @@
+(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.3";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?-1!=n.indexOf(t):E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2);return w.map(n,function(n){return(w.isFunction(t)?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t){return w.isEmpty(t)?[]:w.filter(n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=F(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||void 0===r)return 1;if(e>r||void 0===e)return-1}return n.index
i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i};var I=function(){};w.bind=function(n,t){var r,e;if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));if(!w.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));I.prototype=n.prototype;var u=new I;I.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},w.bindAll=function(n){var t=o.call(arguments,1);return 0==t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=S(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&S(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return S(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),w.isFunction=function(n){return"function"==typeof n},w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return void 0===n},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+(0|Math.random()*(t-n+1))};var T={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};T.unescape=w.invert(T.escape);var M={escape:RegExp("["+w.keys(T.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(T.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(M[n],function(t){return T[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=""+ ++N;return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){r=w.defaults({},r,w.templateSettings);var e=RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(D,function(n){return"\\"+B[n]}),r&&(i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(i+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),a&&(i+="';\n"+a+"\n__p+='"),u=o+t.length,t}),i+="';\n",r.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=Function(r.variable||"obj","_",i)}catch(o){throw o.source=i,o}if(t)return a(t,w);var c=function(n){return a.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+i+"}",c},w.chain=function(n){return w(n).chain()};var z=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);
\ No newline at end of file
diff --git a/php/Services/Soundcloud.php b/php/Services/Soundcloud.php
new file mode 100755
index 0000000..f946ebe
--- /dev/null
+++ b/php/Services/Soundcloud.php
@@ -0,0 +1,950 @@
+
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud
+{
+
+ /**
+ * Custom cURL option
+ *
+ * @var integer
+ *
+ * @access public
+ */
+ const CURLOPT_OAUTH_TOKEN = 173;
+
+ /**
+ * Access token returned by the service provider after a successful authentication
+ *
+ * @var string
+ *
+ * @access private
+ */
+ private $_accessToken;
+
+ /**
+ * Version of the API to use
+ *
+ * @var integer
+ *
+ * @access private
+ * @static
+ */
+ private static $_apiVersion = 1;
+
+ /**
+ * Supported audio MIME types
+ *
+ * @var array
+ *
+ * @access private
+ * @static
+ */
+ private static $_audioMimeTypes = array(
+ 'aac' => 'video/mp4',
+ 'aiff' => 'audio/x-aiff',
+ 'flac' => 'audio/flac',
+ 'mp3' => 'audio/mpeg',
+ 'ogg' => 'audio/ogg',
+ 'wav' => 'audio/x-wav'
+ );
+
+ /**
+ * OAuth client id
+ *
+ * @var string
+ *
+ * @access private
+ */
+ private $_clientId;
+
+ /**
+ * OAuth client secret
+ *
+ * @var string
+ *
+ * @access private
+ */
+ private $_clientSecret;
+
+ /**
+ * Default cURL options
+ *
+ * @var array
+ *
+ * @access private
+ * @static
+ */
+ private static $_curlDefaultOptions = array(
+ CURLOPT_HEADER => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_USERAGENT => ''
+ );
+
+ /**
+ * cURL options
+ *
+ * @var array
+ *
+ * @access private
+ */
+ private $_curlOptions;
+
+ /**
+ * Development mode
+ *
+ * @var boolean
+ *
+ * @access private
+ */
+ private $_development;
+
+ /**
+ * Available API domains
+ *
+ * @var array
+ *
+ * @access private
+ * @static
+ */
+ private static $_domains = array(
+ 'development' => 'sandbox-soundcloud.com',
+ 'production' => 'soundcloud.com'
+ );
+
+ /**
+ * HTTP response body from the last request
+ *
+ * @var string
+ *
+ * @access private
+ */
+ private $_lastHttpResponseBody;
+
+ /**
+ * HTTP response code from the last request
+ *
+ * @var integer
+ *
+ * @access private
+ */
+ private $_lastHttpResponseCode;
+
+ /**
+ * HTTP response headers from last request
+ *
+ * @var array
+ *
+ * @access private
+ */
+ private $_lastHttpResponseHeaders;
+
+ /**
+ * OAuth paths
+ *
+ * @var array
+ *
+ * @access private
+ * @static
+ */
+ private static $_paths = array(
+ 'authorize' => 'connect',
+ 'access_token' => 'oauth2/token',
+ );
+
+ /**
+ * OAuth redirect URI
+ *
+ * @var string
+ *
+ * @access private
+ */
+ private $_redirectUri;
+
+ /**
+ * API response format MIME type
+ *
+ * @var string
+ *
+ * @access private
+ */
+ private $_requestFormat;
+
+ /**
+ * Available response formats
+ *
+ * @var array
+ *
+ * @access private
+ * @static
+ */
+ private static $_responseFormats = array(
+ '*' => '*/*',
+ 'json' => 'application/json',
+ 'xml' => 'application/xml'
+ );
+
+ /**
+ * HTTP user agent
+ *
+ * @var string
+ *
+ * @access private
+ * @static
+ */
+ private static $_userAgent = 'PHP-SoundCloud';
+
+ /**
+ * Class constructor
+ *
+ * @param string $clientId OAuth client id
+ * @param string $clientSecret OAuth client secret
+ * @param string $redirectUri OAuth redirect URI
+ * @param boolean $development Sandbox mode
+ *
+ * @return void
+ * @throws Services_Soundcloud_Missing_Client_Id_Exception
+ *
+ * @access public
+ */
+ function __construct($clientId, $clientSecret, $redirectUri = null, $development = false)
+ {
+ if (empty($clientId)) {
+ throw new Services_Soundcloud_Missing_Client_Id_Exception();
+ }
+
+ $this->_clientId = $clientId;
+ $this->_clientSecret = $clientSecret;
+ $this->_redirectUri = $redirectUri;
+ $this->_development = $development;
+ $this->_responseFormat = self::$_responseFormats['json'];
+ $this->_curlOptions = self::$_curlDefaultOptions;
+ $this->_curlOptions[CURLOPT_USERAGENT] .= $this->_getUserAgent();
+ }
+
+ /**
+ * Get authorization URL
+ *
+ * @param array $params Optional query string parameters
+ *
+ * @return string
+ *
+ * @access public
+ * @see Soundcloud::_buildUrl()
+ */
+ function getAuthorizeUrl($params = array())
+ {
+ $defaultParams = array(
+ 'client_id' => $this->_clientId,
+ 'redirect_uri' => $this->_redirectUri,
+ 'response_type' => 'code'
+ );
+ $params = array_merge($defaultParams, $params);
+
+ return $this->_buildUrl(self::$_paths['authorize'], $params, false);
+ }
+
+ /**
+ * Get access token URL
+ *
+ * @param array $params Optional query string parameters
+ *
+ * @return string
+ *
+ * @access public
+ * @see Soundcloud::_buildUrl()
+ */
+ function getAccessTokenUrl($params = array())
+ {
+ return $this->_buildUrl(self::$_paths['access_token'], $params, false);
+ }
+
+ /**
+ * Retrieve access token through credentials flow
+ *
+ * @param string $username Username
+ * @param string $password Password
+ *
+ * @return mixed
+ *
+ * @access public
+ */
+ function credentialsFlow($username, $password)
+ {
+ $postData = array(
+ 'client_id' => $this->_clientId,
+ 'client_secret' => $this->_clientSecret,
+ 'username' => $username,
+ 'password' => $password,
+ 'grant_type' => 'password'
+ );
+
+ $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
+ $response = json_decode(
+ $this->_request($this->getAccessTokenUrl(), $options),
+ true
+ );
+
+ if (array_key_exists('access_token', $response)) {
+ $this->_accessToken = $response['access_token'];
+
+ return $response;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Retrieve access token
+ *
+ * @param string $code Optional OAuth code returned from the service provider
+ * @param array $postData Optional post data
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_getAccessToken()
+ */
+ function accessToken($code = null, $postData = array(), $curlOptions = array())
+ {
+ $defaultPostData = array(
+ 'code' => $code,
+ 'client_id' => $this->_clientId,
+ 'client_secret' => $this->_clientSecret,
+ 'redirect_uri' => $this->_redirectUri,
+ 'grant_type' => 'authorization_code'
+ );
+ $postData = array_filter(array_merge($defaultPostData, $postData));
+
+ return $this->_getAccessToken($postData, $curlOptions);
+ }
+
+ /**
+ * Refresh access token
+ *
+ * @param string $refreshToken The token to refresh
+ * @param array $postData Optional post data
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ * @see Soundcloud::_getAccessToken()
+ *
+ * @access public
+ */
+ function accessTokenRefresh($refreshToken, $postData = array(), $curlOptions = array())
+ {
+ $defaultPostData = array(
+ 'refresh_token' => $refreshToken,
+ 'client_id' => $this->_clientId,
+ 'client_secret' => $this->_clientSecret,
+ 'redirect_uri' => $this->_redirectUri,
+ 'grant_type' => 'refresh_token'
+ );
+ $postData = array_merge($defaultPostData, $postData);
+
+ return $this->_getAccessToken($postData, $curlOptions);
+ }
+
+ /**
+ * Get access token
+ *
+ * @return mixed
+ *
+ * @access public
+ */
+ function getAccessToken()
+ {
+ return $this->_accessToken;
+ }
+
+ /**
+ * Get API version
+ *
+ * @return integer
+ *
+ * @access public
+ */
+ function getApiVersion()
+ {
+ return self::$_apiVersion;
+ }
+
+ /**
+ * Get the corresponding MIME type for a given file extension
+ *
+ * @param string $extension Given extension
+ *
+ * @return string
+ * @throws Services_Soundcloud_Unsupported_Audio_Format_Exception
+ *
+ * @access public
+ */
+ function getAudioMimeType($extension)
+ {
+ if (array_key_exists($extension, self::$_audioMimeTypes)) {
+ return self::$_audioMimeTypes[$extension];
+ } else {
+ throw new Services_Soundcloud_Unsupported_Audio_Format_Exception();
+ }
+ }
+
+ /**
+ * Get cURL options
+ *
+ * @param string $key Optional options key
+ *
+ * @return mixed
+ *
+ * @access public
+ */
+ function getCurlOptions($key = null)
+ {
+ if ($key) {
+ return (array_key_exists($key, $this->_curlOptions))
+ ? $this->_curlOptions[$key]
+ : false;
+ } else {
+ return $this->_curlOptions;
+ }
+ }
+
+ /**
+ * Get development mode
+ *
+ * @return boolean
+ *
+ * @access public
+ */
+ function getDevelopment()
+ {
+ return $this->_development;
+ }
+
+ /**
+ * Get HTTP response header
+ *
+ * @param string $header Name of the header
+ *
+ * @return mixed
+ *
+ * @access public
+ */
+ function getHttpHeader($header)
+ {
+ if (is_array($this->_lastHttpResponseHeaders)
+ && array_key_exists($header, $this->_lastHttpResponseHeaders)
+ ) {
+ return $this->_lastHttpResponseHeaders[$header];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get redirect URI
+ *
+ * @return string
+ *
+ * @access public
+ */
+ function getRedirectUri()
+ {
+ return $this->_redirectUri;
+ }
+
+ /**
+ * Get response format
+ *
+ * @return string
+ *
+ * @access public
+ */
+ function getResponseFormat()
+ {
+ return $this->_responseFormat;
+ }
+
+ /**
+ * Set access token
+ *
+ * @param string $accessToken Access token
+ *
+ * @return object
+ *
+ * @access public
+ */
+ function setAccessToken($accessToken)
+ {
+ $this->_accessToken = $accessToken;
+
+ return $this;
+ }
+
+ /**
+ * Set cURL options
+ *
+ * The method accepts arguments in two ways.
+ *
+ * You could pass two arguments when adding a single option.
+ *
+ * $soundcloud->setCurlOptions(CURLOPT_SSL_VERIFYHOST, 0);
+ *
+ *
+ * You could also pass an associative array when adding multiple options.
+ *
+ * $soundcloud->setCurlOptions(array(
+ * CURLOPT_SSL_VERIFYHOST => 0,
+ * CURLOPT_SSL_VERIFYPEER => 0
+ * ));
+ *
+ *
+ * @return object
+ *
+ * @access public
+ */
+ function setCurlOptions()
+ {
+ $args = func_get_args();
+ $options = (is_array($args[0]))
+ ? $args[0]
+ : array($args[0] => $args[1]);
+
+ foreach ($options as $key => $val) {
+ $this->_curlOptions[$key] = $val;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set redirect URI
+ *
+ * @param string $redirectUri Redirect URI
+ *
+ * @return object
+ *
+ * @access public
+ */
+ function setRedirectUri($redirectUri)
+ {
+ $this->_redirectUri = $redirectUri;
+
+ return $this;
+ }
+
+ /**
+ * Set response format
+ *
+ * @param string $format Response format, could either be XML or JSON
+ *
+ * @return object
+ * @throws Services_Soundcloud_Unsupported_Response_Format_Exception
+ *
+ * @access public
+ */
+ function setResponseFormat($format)
+ {
+ if (array_key_exists($format, self::$_responseFormats)) {
+ $this->_responseFormat = self::$_responseFormats[$format];
+ } else {
+ throw new Services_Soundcloud_Unsupported_Response_Format_Exception();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set development mode
+ *
+ * @param boolean $development Development mode
+ *
+ * @return object
+ *
+ * @access public
+ */
+ function setDevelopment($development)
+ {
+ $this->_development = $development;
+
+ return $this;
+ }
+
+ /**
+ * Send a GET HTTP request
+ *
+ * @param string $path Request path
+ * @param array $params Optional query string parameters
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_request()
+ */
+ function get($path, $params = array(), $curlOptions = array())
+ {
+ $url = $this->_buildUrl($path, $params);
+
+ return $this->_request($url, $curlOptions);
+ }
+
+ /**
+ * Send a POST HTTP request
+ *
+ * @param string $path Request path
+ * @param array $postData Optional post data
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_request()
+ */
+ function post($path, $postData = array(), $curlOptions = array())
+ {
+ $url = $this->_buildUrl($path);
+ $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
+ $options += $curlOptions;
+
+ return $this->_request($url, $options);
+ }
+
+ /**
+ * Send a PUT HTTP request
+ *
+ * @param string $path Request path
+ * @param array $postData Optional post data
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_request()
+ */
+ function put($path, $postData, $curlOptions = array())
+ {
+ $url = $this->_buildUrl($path);
+ $options = array(
+ CURLOPT_CUSTOMREQUEST => 'PUT',
+ CURLOPT_POSTFIELDS => $postData
+ );
+ $options += $curlOptions;
+
+ return $this->_request($url, $options);
+ }
+
+ /**
+ * Send a DELETE HTTP request
+ *
+ * @param string $path Request path
+ * @param array $params Optional query string parameters
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_request()
+ */
+ function delete($path, $params = array(), $curlOptions = array())
+ {
+ $url = $this->_buildUrl($path, $params);
+ $options = array(CURLOPT_CUSTOMREQUEST => 'DELETE');
+ $options += $curlOptions;
+
+ return $this->_request($url, $options);
+ }
+
+ /**
+ * Download track
+ *
+ * @param integer $trackId Track id to download
+ * @param array $params Optional query string parameters
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_request()
+ */
+ function download($trackId, $params = array(), $curlOptions = array())
+ {
+ $lastResponseFormat = array_pop(explode('/', $this->getResponseFormat()));
+ $defaultParams = array('oauth_token' => $this->getAccessToken());
+ $defaultCurlOptions = array(
+ CURLOPT_FOLLOWLOCATION => true,
+ self::CURLOPT_OAUTH_TOKEN => false
+ );
+ $url = $this->_buildUrl(
+ 'tracks/' . $trackId . '/download',
+ array_merge($defaultParams, $params)
+ );
+ $options = $defaultCurlOptions + $curlOptions;
+
+ $this->setResponseFormat('*');
+
+ $response = $this->_request($url, $options);
+
+ // rollback to the previously defined response format.
+ $this->setResponseFormat($lastResponseFormat);
+
+ return $response;
+ }
+
+ /**
+ * Update a existing playlist
+ *
+ * @param integer $playlistId The playlist id
+ * @param array $trackIds Tracks to add to the playlist
+ * @param array $optionalPostData Optional playlist fields to update
+ *
+ * @return mixed
+ *
+ * @access public
+ * @see Soundcloud::_request()
+ */
+ public function updatePlaylist($playlistId, $trackIds, $optionalPostData = null)
+ {
+ $url = $this->_buildUrl('playlists/' . $playlistId);
+ $postData = array();
+
+ foreach ($trackIds as $trackId) {
+ array_push($postData, 'playlist[tracks][][id]=' . $trackId);
+ }
+
+ if (is_array($optionalPostData)) {
+ foreach ($optionalPostData as $key => $val) {
+ array_push($postData, 'playlist[' . $key . ']=' . $val);
+ }
+ }
+
+ $postData = implode('&', $postData);
+ $curlOptions = array(
+ CURLOPT_CUSTOMREQUEST => 'PUT',
+ CURLOPT_HTTPHEADER => array('Content-Length' => strlen($postData)),
+ CURLOPT_POSTFIELDS => $postData
+ );
+
+ return $this->_request($url, $curlOptions);
+ }
+
+ /**
+ * Construct default HTTP request headers
+ *
+ * @param boolean $includeAccessToken Include access token
+ *
+ * @return array $headers
+ *
+ * @access protected
+ */
+ protected function _buildDefaultHeaders($includeAccessToken = true)
+ {
+ $headers = array();
+
+ if ($this->_responseFormat) {
+ array_push($headers, 'Accept: ' . $this->_responseFormat);
+ }
+
+ if ($includeAccessToken && $this->_accessToken) {
+ array_push($headers, 'Authorization: OAuth ' . $this->_accessToken);
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Construct a URL
+ *
+ * @param string $path Relative or absolute URI
+ * @param array $params Optional query string parameters
+ * @param boolean $includeVersion Include API version
+ *
+ * @return string $url
+ *
+ * @access protected
+ */
+ protected function _buildUrl($path, $params = array(), $includeVersion = true)
+ {
+ if (!$this->_accessToken) {
+ $params['consumer_key'] = $this->_clientId;
+ }
+
+ if (preg_match('/^https?\:\/\//', $path)) {
+ $url = $path;
+ } else {
+ $url = 'https://';
+ $url .= (!preg_match('/connect/', $path)) ? 'api.' : '';
+ $url .= ($this->_development)
+ ? self::$_domains['development']
+ : self::$_domains['production'];
+ $url .= '/';
+ $url .= ($includeVersion) ? 'v' . self::$_apiVersion . '/' : '';
+ $url .= $path;
+ }
+
+ $url .= (count($params)) ? '?' . http_build_query($params) : '';
+
+ return $url;
+ }
+
+ /**
+ * Retrieve access token
+ *
+ * @param array $postData Post data
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ *
+ * @access protected
+ */
+ protected function _getAccessToken($postData, $curlOptions = array())
+ {
+ $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
+ $options += $curlOptions;
+ $response = json_decode(
+ $this->_request($this->getAccessTokenUrl(), $options),
+ true
+ );
+
+ if (array_key_exists('access_token', $response)) {
+ $this->_accessToken = $response['access_token'];
+
+ return $response;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get HTTP user agent
+ *
+ * @return string
+ *
+ * @access protected
+ */
+ protected function _getUserAgent()
+ {
+ return self::$_userAgent . '/' . new Services_Soundcloud_Version;
+ }
+
+ /**
+ * Parse HTTP headers
+ *
+ * @param string $headers HTTP headers
+ *
+ * @return array $parsedHeaders
+ *
+ * @access protected
+ */
+ protected function _parseHttpHeaders($headers)
+ {
+ $headers = explode("\n", trim($headers));
+ $parsedHeaders = array();
+
+ foreach ($headers as $header) {
+ if (!preg_match('/\:\s/', $header)) {
+ continue;
+ }
+
+ list($key, $val) = explode(': ', $header, 2);
+ $key = str_replace('-', '_', strtolower($key));
+ $val = trim($val);
+
+ $parsedHeaders[$key] = $val;
+ }
+
+ return $parsedHeaders;
+ }
+
+ /**
+ * Validate HTTP response code
+ *
+ * @param integer $code HTTP code
+ *
+ * @return boolean
+ *
+ * @access protected
+ */
+ protected function _validResponseCode($code)
+ {
+ return (bool)preg_match('/^20[0-9]{1}$/', $code);
+ }
+
+ /**
+ * Performs the actual HTTP request using cURL
+ *
+ * @param string $url Absolute URL to request
+ * @param array $curlOptions Optional cURL options
+ *
+ * @return mixed
+ * @throws Services_Soundcloud_Invalid_Http_Response_Code_Exception
+ *
+ * @access protected
+ */
+ protected function _request($url, $curlOptions = array())
+ {
+ $ch = curl_init($url);
+ $options = $this->_curlOptions;
+ $options += $curlOptions;
+
+ if (array_key_exists(self::CURLOPT_OAUTH_TOKEN, $options)) {
+ $includeAccessToken = $options[self::CURLOPT_OAUTH_TOKEN];
+ unset($options[self::CURLOPT_OAUTH_TOKEN]);
+ } else {
+ $includeAccessToken = true;
+ }
+
+ if (array_key_exists(CURLOPT_HTTPHEADER, $options)) {
+ $options[CURLOPT_HTTPHEADER] = array_merge(
+ $this->_buildDefaultHeaders(),
+ $curlOptions[CURLOPT_HTTPHEADER]
+ );
+ } else {
+ $options[CURLOPT_HTTPHEADER] = $this->_buildDefaultHeaders(
+ $includeAccessToken
+ );
+ }
+
+ curl_setopt_array($ch, $options);
+
+ $data = curl_exec($ch);
+ $info = curl_getinfo($ch);
+
+ curl_close($ch);
+
+ if (array_key_exists(CURLOPT_HEADER, $options) && $options[CURLOPT_HEADER]) {
+ $this->_lastHttpResponseHeaders = $this->_parseHttpHeaders(
+ substr($data, 0, $info['header_size'])
+ );
+ $this->_lastHttpResponseBody = substr($data, $info['header_size']);
+ } else {
+ $this->_lastHttpResponseHeaders = array();
+ $this->_lastHttpResponseBody = $data;
+ }
+
+ $this->_lastHttpResponseCode = $info['http_code'];
+
+ if ($this->_validResponseCode($this->_lastHttpResponseCode)) {
+ return $this->_lastHttpResponseBody;
+ } else {
+ throw new Services_Soundcloud_Invalid_Http_Response_Code_Exception(
+ null,
+ 0,
+ $this->_lastHttpResponseBody,
+ $this->_lastHttpResponseCode
+ );
+ }
+ }
+
+}
diff --git a/php/Services/Soundcloud/Exception.php b/php/Services/Soundcloud/Exception.php
new file mode 100755
index 0000000..76e3370
--- /dev/null
+++ b/php/Services/Soundcloud/Exception.php
@@ -0,0 +1,146 @@
+
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud_Missing_Client_Id_Exception extends Exception {
+
+ /**
+ * Default message.
+ *
+ * @access protected
+ *
+ * @var string
+ */
+ protected $message = 'All requests must include a consumer key. Referred to as client_id in OAuth2.';
+
+}
+
+/**
+ * Soundcloud invalid HTTP response code exception.
+ *
+ * @category Services
+ * @package Services_Soundcloud
+ * @author Anton Lindqvist
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud_Invalid_Http_Response_Code_Exception extends Exception {
+
+ /**
+ * HTTP response body.
+ *
+ * @access protected
+ *
+ * @var string
+ */
+ protected $httpBody;
+
+ /**
+ * HTTP response code.
+ *
+ * @access protected
+ *
+ * @var integer
+ */
+ protected $httpCode;
+
+ /**
+ * Default message.
+ *
+ * @access protected
+ *
+ * @var string
+ */
+ protected $message = 'The requested URL responded with HTTP code %d.';
+
+ /**
+ * Constructor.
+ *
+ * @param string $message
+ * @param string $code
+ * @param string $httpBody
+ * @param integer $httpCode
+ *
+ * @return void
+ */
+ function __construct($message = null, $code = 0, $httpBody = null, $httpCode = 0) {
+ $this->httpBody = $httpBody;
+ $this->httpCode = $httpCode;
+ $message = sprintf($this->message, $httpCode);
+
+ parent::__construct($message, $code);
+ }
+
+ /**
+ * Get HTTP response body.
+ *
+ * @return mixed
+ */
+ function getHttpBody() {
+ return $this->httpBody;
+ }
+
+ /**
+ * Get HTTP response code.
+ *
+ * @return mixed
+ */
+ function getHttpCode() {
+ return $this->httpCode;
+ }
+
+}
+
+/**
+ * Soundcloud unsupported response format exception.
+ *
+ * @category Services
+ * @package Services_Soundcloud
+ * @author Anton Lindqvist
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud_Unsupported_Response_Format_Exception extends Exception {
+
+ /**
+ * Default message.
+ *
+ * @access protected
+ *
+ * @var string
+ */
+ protected $message = 'The given response format is unsupported.';
+
+}
+
+/**
+ * Soundcloud unsupported audio format exception.
+ *
+ * @category Services
+ * @package Services_Soundcloud
+ * @author Anton Lindqvist
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud_Unsupported_Audio_Format_Exception extends Exception {
+
+ /**
+ * Default message.
+ *
+ * @access protected
+ *
+ * @var string
+ */
+ protected $message = 'The given audio format is unsupported.';
+
+}
diff --git a/php/Services/Soundcloud/Version.php b/php/Services/Soundcloud/Version.php
new file mode 100755
index 0000000..c79a3fd
--- /dev/null
+++ b/php/Services/Soundcloud/Version.php
@@ -0,0 +1,31 @@
+
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud_Version
+{
+
+ const MAJOR = 2;
+ const MINOR = 3;
+ const PATCH = 2;
+
+ /**
+ * Magic to string method
+ *
+ * @return string
+ *
+ * @access public
+ */
+ function __toString()
+ {
+ return implode('.', array(self::MAJOR, self::MINOR, self::PATCH));
+ }
+
+}
diff --git a/php/tests/Soundcloud_Test.php b/php/tests/Soundcloud_Test.php
new file mode 100755
index 0000000..9b8d604
--- /dev/null
+++ b/php/tests/Soundcloud_Test.php
@@ -0,0 +1,348 @@
+soundcloud = new Services_Soundcloud_Expose(
+ '1337',
+ '1337',
+ 'http://soundcloud.local/callback'
+ );
+
+ $this->soundcloud->setAccessToken('1337');
+ }
+
+ function tearDown() {
+ $this->soundcloud = null;
+ }
+
+ function testVersionFormat() {
+ self::assertRegExp(
+ '/^[0-9]+\.[0-9]+\.[0-9]+$/',
+ (string)new Services_Soundcloud_Version
+ );
+ }
+
+ function testGetUserAgent() {
+ self::assertRegExp(
+ '/^PHP\-SoundCloud\/[0-9]+\.[0-9]+\.[0-9]+$/',
+ $this->soundcloud->getUserAgent()
+ );
+ }
+
+ function testApiVersion() {
+ self::assertEquals(1, $this->soundcloud->getApiVersion());
+ }
+
+ function testGetAudioMimeTypes() {
+ $supportedExtensions = array(
+ 'aac' => 'video/mp4',
+ 'aiff' => 'audio/x-aiff',
+ 'flac' => 'audio/flac',
+ 'mp3' => 'audio/mpeg',
+ 'ogg' => 'audio/ogg',
+ 'wav' => 'audio/x-wav'
+ );
+ $unsupportedExtensions = array('gif', 'html', 'jpg', 'mp4', 'xml', 'xspf');
+
+ foreach ($supportedExtensions as $extension => $mimeType) {
+ self::assertEquals(
+ $mimeType,
+ $this->soundcloud->getAudioMimeType($extension)
+ );
+ }
+
+ foreach ($unsupportedExtensions as $extension => $mimeType) {
+ $this->setExpectedException('Services_Soundcloud_Unsupported_Audio_Format_Exception');
+
+ $this->soundcloud->getAudioMimeType($extension);
+ }
+ }
+
+ function testGetAuthorizeUrl() {
+ self::assertEquals(
+ 'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code',
+ $this->soundcloud->getAuthorizeUrl()
+ );
+ }
+
+ function testGetAuthorizeUrlWithCustomQueryParameters() {
+ self::assertEquals(
+ 'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code&foo=bar',
+ $this->soundcloud->getAuthorizeUrl(array('foo' => 'bar'))
+ );
+
+ self::assertEquals(
+ 'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code&foo=bar&bar=foo',
+ $this->soundcloud->getAuthorizeUrl(array('foo' => 'bar', 'bar' => 'foo'))
+ );
+ }
+
+ function testGetAccessTokenUrl() {
+ self::assertEquals(
+ 'https://api.soundcloud.com/oauth2/token',
+ $this->soundcloud->getAccessTokenUrl()
+ );
+ }
+
+ function testSetAccessToken() {
+ $this->soundcloud->setAccessToken('1337');
+
+ self::assertEquals('1337', $this->soundcloud->getAccessToken());
+ }
+
+ function testSetCurlOptions() {
+ $this->soundcloud->setCurlOptions(CURLOPT_SSL_VERIFYHOST, 0);
+
+ self::assertEquals(
+ 0,
+ $this->soundcloud->getCurlOptions(CURLOPT_SSL_VERIFYHOST)
+ );
+ }
+
+ function testSetCurlOptionsArray() {
+ $options = array(
+ CURLOPT_SSL_VERIFYHOST => 0,
+ CURLOPT_SSL_VERIFYPEER => 0
+ );
+
+ $this->soundcloud->setCurlOptions($options);
+
+ foreach ($options as $key => $val) {
+ self::assertEquals(
+ $val,
+ $this->soundcloud->getCurlOptions($key)
+ );
+ }
+ }
+
+ function testSetDevelopment() {
+ $this->soundcloud->setDevelopment(true);
+
+ self::assertTrue($this->soundcloud->getDevelopment());
+ }
+
+ function testSetRedirectUri() {
+ $this->soundcloud->setRedirectUri('http://soundcloud.local/callback');
+
+ self::assertEquals(
+ 'http://soundcloud.local/callback',
+ $this->soundcloud->getRedirectUri()
+ );
+ }
+
+ function testDefaultResponseFormat() {
+ self::assertEquals(
+ 'application/json',
+ $this->soundcloud->getResponseFormat()
+ );
+ }
+
+ function testSetResponseFormatHtml() {
+ $this->setExpectedException('Services_Soundcloud_Unsupported_Response_Format_Exception');
+
+ $this->soundcloud->setResponseFormat('html');
+ }
+
+ function testSetResponseFormatAll() {
+ $this->soundcloud->setResponseFormat('*');
+
+ self::assertEquals(
+ '*/*',
+ $this->soundcloud->getResponseFormat()
+ );
+ }
+
+ function testSetResponseFormatJson() {
+ $this->soundcloud->setResponseFormat('json');
+
+ self::assertEquals(
+ 'application/json',
+ $this->soundcloud->getResponseFormat()
+ );
+ }
+
+ function testSetResponseFormatXml() {
+ $this->soundcloud->setResponseFormat('xml');
+
+ self::assertEquals(
+ 'application/xml',
+ $this->soundcloud->getResponseFormat()
+ );
+ }
+
+ function testResponseCodeSuccess() {
+ self::assertTrue($this->soundcloud->validResponseCode(200));
+ }
+
+ function testResponseCodeRedirect() {
+ self::assertFalse($this->soundcloud->validResponseCode(301));
+ }
+
+ function testResponseCodeClientError() {
+ self::assertFalse($this->soundcloud->validResponseCode(400));
+ }
+
+ function testResponseCodeServerError() {
+ self::assertFalse($this->soundcloud->validResponseCode(500));
+ }
+
+ function testBuildDefaultHeaders() {
+ $this->soundcloud->setAccessToken(null);
+
+ self::assertEquals(
+ array('Accept: application/json'),
+ $this->soundcloud->buildDefaultHeaders()
+ );
+ }
+
+ function testBuildDefaultHeadersWithAccessToken() {
+ $this->soundcloud->setAccessToken('1337');
+
+ self::assertEquals(
+ array('Accept: application/json', 'Authorization: OAuth 1337'),
+ $this->soundcloud->buildDefaultHeaders()
+ );
+ }
+
+ function testBuildUrl() {
+ self::assertEquals(
+ 'https://api.soundcloud.com/v1/me',
+ $this->soundcloud->buildUrl('me')
+ );
+ }
+
+ function testBuildUrlWithQueryParameters() {
+ self::assertEquals(
+ 'https://api.soundcloud.com/v1/tracks?q=rofl+dubstep',
+ $this->soundcloud->buildUrl(
+ 'tracks',
+ array('q' => 'rofl dubstep')
+ )
+ );
+
+ self::assertEquals(
+ 'https://api.soundcloud.com/v1/tracks?q=rofl+dubstep&filter=public',
+ $this->soundcloud->buildUrl(
+ 'tracks',
+ array('q' => 'rofl dubstep', 'filter' => 'public')
+ )
+ );
+ }
+
+ function testBuildUrlWithDevelopmentDomain() {
+ $this->soundcloud->setDevelopment(true);
+
+ self::assertEquals(
+ 'https://api.sandbox-soundcloud.com/v1/me',
+ $this->soundcloud->buildUrl('me')
+ );
+ }
+
+ function testBuildUrlWithoutApiVersion() {
+ self::assertEquals(
+ 'https://api.soundcloud.com/me',
+ $this->soundcloud->buildUrl('me', null, false)
+ );
+ }
+
+ function testBuildUrlWithAbsoluteUrl() {
+ self::assertEquals(
+ 'https://api.soundcloud.com/me',
+ $this->soundcloud->buildUrl('https://api.soundcloud.com/me')
+ );
+ }
+
+ function testBuildUrlWithoutAccessToken() {
+ $this->soundcloud->setAccessToken(null);
+
+ self::assertEquals(
+ 'https://api.soundcloud.com/v1/tracks?consumer_key=1337',
+ $this->soundcloud->buildUrl('tracks')
+ );
+ }
+
+ /**
+ * @dataProvider dataProviderHttpHeaders
+ */
+ function testParseHttpHeaders($rawHeaders, $expectedHeaders) {
+ $parsedHeaders = $this->soundcloud->parseHttpHeaders($rawHeaders);
+
+ foreach ($parsedHeaders as $key => $val) {
+ self::assertEquals($val, $expectedHeaders[$key]);
+ }
+ }
+
+ function testSoundcloudMissingConsumerKeyException() {
+ $this->setExpectedException('Services_Soundcloud_Missing_Client_Id_Exception');
+
+ $soundcloud = new Services_Soundcloud('', '');
+ }
+
+ function testSoundcloudInvalidHttpResponseCodeException() {
+ $this->setExpectedException('Services_Soundcloud_Invalid_Http_Response_Code_Exception');
+
+ $this->soundcloud->get('me');
+ }
+
+ /**
+ * @dataProvider dataProviderSoundcloudInvalidHttpResponseCode
+ */
+ function testSoundcloudInvalidHttpResponseCode($expectedHeaders) {
+ try {
+ $this->soundcloud->get('me');
+ } catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
+ self::assertEquals(
+ '{"error":"401 - Unauthorized"}',
+ $e->getHttpBody()
+ );
+
+ self::assertEquals(401, $e->getHttpCode());
+
+ foreach ($expectedHeaders as $key => $val) {
+ self::assertEquals(
+ $val,
+ $this->soundcloud->getHttpHeader($key)
+ );
+ }
+ }
+ }
+
+ static function dataProviderHttpHeaders() {
+ $rawHeaders = <<<'HEADERS'
+HTTP/1.1 200 OK
+Date: Wed, 17 Nov 2010 15:39:52 GMT
+Cache-Control: public
+Content-Type: text/html; charset=utf-8
+Content-Encoding: gzip
+Server: foobar
+Content-Length: 1337
+HEADERS;
+ $expectedHeaders = array(
+ 'date' => 'Wed, 17 Nov 2010 15:39:52 GMT',
+ 'cache_control' => 'public',
+ 'content_type' => 'text/html; charset=utf-8',
+ 'content_encoding' => 'gzip',
+ 'server' => 'foobar',
+ 'content_length' => '1337'
+ );
+
+ return array(array($rawHeaders, $expectedHeaders));
+ }
+
+ static function dataProviderSoundcloudInvalidHttpResponseCode() {
+ $expectedHeaders = array(
+ 'server' => 'nginx',
+ 'content_type' => 'application/json; charset=utf-8',
+ 'connection' => 'keep-alive',
+ 'cache_control' => 'no-cache',
+ 'content_length' => '30'
+ );
+
+ return array(array($expectedHeaders));
+ }
+
+}
diff --git a/php/tests/Soundcloud_Test_Helper.php b/php/tests/Soundcloud_Test_Helper.php
new file mode 100755
index 0000000..4a101af
--- /dev/null
+++ b/php/tests/Soundcloud_Test_Helper.php
@@ -0,0 +1,94 @@
+
+ * @copyright 2010 Anton Lindqvist
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ * @link http://github.com/mptre/php-soundcloud
+ */
+class Services_Soundcloud_Expose extends Services_Soundcloud {
+
+ /**
+ * Class constructor. See parent constructor for further reference.
+ *
+ * @param string $clientId Application client id
+ * @param string $clientSecret Application client secret
+ * @param string $redirectUri Application redirect uri
+ * @param boolean $development Sandbox mode
+ *
+ * @return void
+ * @see Soundcloud
+ */
+ function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) {
+ parent::__construct($clientId, $clientSecret, $redirectUri, $development);
+ }
+
+ /**
+ * Construct default http headers including response format and authorization.
+ *
+ * @return array
+ * @see Soundcloud::_buildDefaultHeaders()
+ */
+ function buildDefaultHeaders() {
+ return $this->_buildDefaultHeaders();
+ }
+
+ /**
+ * Construct a url.
+ *
+ * @param string $path Relative or absolute uri
+ * @param array $params Optional query string parameters
+ * @param boolean $includeVersion Include the api version
+ *
+ * @return string
+ * @see Soundcloud::_buildUrl()
+ */
+ function buildUrl($path, $params = null, $includeVersion = true) {
+ return $this->_buildUrl($path, $params, $includeVersion);
+ }
+
+ /**
+ * Get http user agent.
+ *
+ * @return string
+ * @see Soundcloud::_getUserAgent()
+ */
+ function getUserAgent() {
+ return $this->_getUserAgent();
+ }
+
+ /**
+ * Parse HTTP response headers.
+ *
+ * @param string $headers
+ *
+ * @return array
+ * @see Soundcloud::_parseHttpHeaders()
+ */
+ function parseHttpHeaders($headers) {
+ return $this->_parseHttpHeaders($headers);
+ }
+
+ /**
+ * Validates http response code.
+ *
+ * @return boolean
+ * @see Soundcloud::_validResponseCode()
+ */
+ function validResponseCode($code) {
+ return $this->_validResponseCode($code);
+ }
+
+}
diff --git a/php/underscore.php b/php/underscore.php
new file mode 100755
index 0000000..39b22a8
--- /dev/null
+++ b/php/underscore.php
@@ -0,0 +1,1110 @@
+ 0) $__->_wrapped = $item;
+ return $__;
+}
+
+// Underscore.php
+class __ {
+
+ // Start the chain
+ private $_chained = false; // Are we in a chain?
+ public function chain($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+
+ $__ = (isset($this) && isset($this->_chained) && $this->_chained) ? $this : __($item);
+ $__->_chained = true;
+ return $__;
+ }
+
+
+ // End the chain
+ public function value() {
+ return (isset($this)) ? $this->_wrapped : null;
+ }
+
+
+ // Invoke the iterator on each item in the collection
+ public function each($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ if(is_null($collection)) return self::_wrap(null);
+
+ $collection = (array) self::_collection($collection);
+ if(count($collection) === 0) return self::_wrap(null);
+
+ foreach($collection as $k=>$v) {
+ call_user_func($iterator, $v, $k, $collection);
+ }
+ return self::_wrap(null);
+ }
+
+
+ // Return an array of values by mapping each item through the iterator
+ // map alias: collect
+ public function collect($collection=null, $iterator=null) { return self::map($collection, $iterator); }
+ public function map($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ if(is_null($collection)) return self::_wrap(array());
+
+ $collection = (array) self::_collection($collection);
+ if(count($collection) === 0) self::_wrap(array());
+
+ $return = array();
+ foreach($collection as $k=>$v) {
+ $return[] = call_user_func($iterator, $v, $k, $collection);
+ }
+ return self::_wrap($return);
+ }
+
+
+ // Reduce a collection to a single value
+ // reduce aliases: foldl, inject
+ public function foldl($collection=null, $iterator=null, $memo=null) { return self::reduce($collection, $iterator, $memo); }
+ public function inject($collection=null, $iterator=null, $memo=null) { return self::reduce($collection, $iterator, $memo); }
+ public function reduce($collection=null, $iterator=null, $memo=null) {
+ list($collection, $iterator, $memo) = self::_wrapArgs(func_get_args(), 3);
+
+ if(!is_object($collection) && !is_array($collection)) {
+ if(is_null($memo)) throw new Exception('Invalid object');
+ else return self::_wrap($memo);
+ }
+
+ return self::_wrap(array_reduce($collection, $iterator, $memo));
+ }
+
+
+ // Right-associative version of reduce
+ // reduceRight alias: foldr
+ public function foldr($collection=null, $iterator=null, $memo=null) { return self::reduceRight($collection, $iterator, $memo); }
+ public function reduceRight($collection=null, $iterator=null, $memo=null) {
+ list($collection, $iterator, $memo) = self::_wrapArgs(func_get_args(), 3);
+
+ if(!is_object($collection) && !is_array($collection)) {
+ if(is_null($memo)) throw new Exception('Invalid object');
+ else return self::_wrap($memo);
+ }
+
+ krsort($collection);
+
+ $__ = new self;
+ return self::_wrap($__->reduce($collection, $iterator, $memo));
+ }
+
+
+ // Extract an array of values for a given property
+ public function pluck($collection=null, $key=null) {
+ list($collection, $key) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = (array) self::_collection($collection);
+
+ $return = array();
+ foreach($collection as $item) {
+ foreach($item as $k=>$v) {
+ if($k === $key) $return[] = $v;
+ }
+ }
+ return self::_wrap($return);
+ }
+
+
+ // Does the collection contain this value?
+ // includ alias: contains
+ public function contains($collection=null, $val=null) { return self::includ($collection, $val); }
+ public function includ($collection=null, $val=null) {
+ list($collection, $val) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = (array) self::_collection($collection);
+
+ return self::_wrap((array_search($val, $collection, true) !== false));
+ }
+
+
+ // Invoke the named function over each item in the collection, optionally passing arguments to the function
+ public function invoke($collection=null, $function_name=null, $arguments=null) {
+ $args = self::_wrapArgs(func_get_args(), 2);
+ $__ = new self;
+ list($collection, $function_name) = $__->first($args, 2);
+ $arguments = $__->rest(func_get_args(), 2);
+
+ // If passed an array or string, return an array
+ // If passed an object, return an object
+ $is_obj = is_object($collection);
+ $result = (empty($arguments)) ? array_map($function_name, (array) $collection) : array_map($function_name, (array) $collection, $arguments);
+ if($is_obj) $result = (object) $result;
+
+ return self::_wrap($result);
+ }
+
+
+ // Does any values in the collection meet the iterator's truth test?
+ // any alias: some
+ public function some($collection=null, $iterator=null) { return self::any($collection, $iterator); }
+ public function any($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ $__ = new self;
+ if(!is_null($iterator)) $collection = $__->map($collection, $iterator);
+ if(count($collection) === 0) return self::_wrap(false);
+
+ return self::_wrap(is_int(array_search(true, $collection, false)));
+ }
+
+
+ // Do all values in the collection meet the iterator's truth test?
+ // all alias: every
+ public function every($collection=null, $iterator=null) { return self::all($collection, $iterator); }
+ public function all($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ $__ = new self;
+ if(!is_null($iterator)) $collection = $__->map($collection, $iterator);
+ $collection = (array) $collection;
+ if(count($collection) === 0) return true;
+
+ return self::_wrap(is_bool(array_search(false, $collection, false)));
+ }
+
+
+ // Return an array of values that pass the truth iterator test
+ // filter alias: select
+ public function select($collection=null, $iterator=null) { return self::filter($collection, $iterator); }
+ public function filter($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ $return = array();
+ foreach($collection as $val) {
+ if(call_user_func($iterator, $val)) $return[] = $val;
+ }
+ return self::_wrap($return);
+ }
+
+
+ // Return an array where the items failing the truth test are removed
+ public function reject($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ $return = array();
+ foreach($collection as $val) {
+ if(!call_user_func($iterator, $val)) $return[] = $val;
+ }
+ return self::_wrap($return);
+ }
+
+
+ // Return the value of the first item passing the truth iterator test
+ // find alias: detect
+ public function detect($collection=null, $iterator=null) { return self::find($collection, $iterator); }
+ public function find($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ foreach($collection as $val) {
+ if(call_user_func($iterator, $val)) return $val;
+ }
+ return self::_wrap(false);
+ }
+
+
+ // How many items are in this collection?
+ public function size($collection=null) {
+ list($collection) = self::_wrapArgs(func_get_args(), 1);
+
+ $collection = self::_collection($collection);
+
+ return self::_wrap(count((array) $collection));
+ }
+
+
+ // Get the first element of an array. Passing n returns the first n elements.
+ // first alias: head
+ public function head($collection=null, $n=null) { return self::first($collection, $n); }
+ public function first($collection=null, $n=null) {
+ list($collection, $n) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ if($n === 0) return self::_wrap(array());
+ if(is_null($n)) return self::_wrap(current(array_splice($collection, 0, 1, true)));
+ return self::_wrap(array_splice($collection, 0, $n, true));
+ }
+
+
+ // Get the rest of the array elements. Passing n returns from that index onward.
+ public function tail($collection=null, $index=null) { return self::rest($collection, $index); }
+ public function rest($collection=null, $index=null) {
+ list($collection, $index) = self::_wrapArgs(func_get_args(), 2);
+ if(is_null($index)) $index = 1;
+
+ $collection = self::_collection($collection);
+
+ return self::_wrap(array_splice($collection, $index));
+ }
+
+
+ // Return everything but the last array element. Passing n excludes the last n elements.
+ public function initial($collection=null, $n=null) {
+ list($collection, $n) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = (array) self::_collection($collection);
+
+ if(is_null($n)) $n = 1;
+ $first_index = count($collection) - $n;
+ $__ = new self;
+ return self::_wrap($__->first($collection, $first_index));
+ }
+
+
+ // Get the last element from an array. Passing n returns the last n elements.
+ public function last($collection=null, $n=null) {
+ list($collection, $n) = self::_wrapArgs(func_get_args(), 2);
+ $collection = self::_collection($collection);
+
+ if($n === 0) $result = array();
+ elseif($n === 1 || is_null($n)) $result = array_pop($collection);
+ else {
+ $__ = new self;
+ $result = $__->rest($collection, -$n);
+ }
+
+ return self::_wrap($result);
+ }
+
+
+
+
+
+ // Flattens a multidimensional array
+ public function flatten($collection=null, $shallow=null) {
+ list($collection, $shallow) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ $return = array();
+ if(count($collection) > 0) {
+ foreach($collection as $item) {
+ if(is_array($item)) {
+ $__ = new self;
+ $return = array_merge($return, ($shallow) ? $item : $__->flatten($item));
+ }
+ else $return[] = $item;
+ }
+ }
+ return self::_wrap($return);
+ }
+
+
+ // Returns a copy of the array with all instances of val removed
+ public function without($collection=null, $val=null) {
+ $args = self::_wrapArgs(func_get_args(), 1);
+ $collection = $args[0];
+ $collection = self::_collection($collection);
+
+ $num_args = count($args);
+ if($num_args === 1) return self::_wrap($collection);
+ if(count($collection) === 0) return self::_wrap($collection);
+
+ $__ = new self;
+ $removes = $__->rest($args);
+ foreach($removes as $remove) {
+ $remove_keys = array_keys($collection, $remove, true);
+ if(count($remove_keys) > 0) {
+ foreach($remove_keys as $key) {
+ unset($collection[$key]);
+ }
+ }
+ }
+ return self::_wrap($collection);
+ }
+
+
+ // Return an array of the unique values
+ // uniq alias: unique
+ public function unique($collection=null, $is_sorted=null, $iterator=null) { return self::uniq($collection, $is_sorted, $iterator); }
+ public function uniq($collection=null, $is_sorted=null, $iterator=null) {
+ list($collection, $is_sorted, $iterator) = self::_wrapArgs(func_get_args(), 3);
+
+ $collection = self::_collection($collection);
+
+ $return = array();
+ if(count($collection) === 0) return self::_wrap($return);
+
+ $calculated = array();
+ foreach($collection as $item) {
+ $val = (!is_null($iterator)) ? $iterator($item) : $item;
+ if(is_bool(array_search($val, $calculated, true))) {
+ $calculated[] = $val;
+ $return[] = $item;
+ }
+ }
+
+ return self::_wrap($return);
+ }
+
+
+ // Returns an array containing the intersection of all the arrays
+ public function intersection($array=null) {
+ $arrays = self::_wrapArgs(func_get_args(), 1);
+
+ if(count($arrays) === 1) return self::_wrap($array);
+
+ $__ = new self;
+ $return = $__->first($arrays);
+ foreach($__->rest($arrays) as $next) {
+ if(!$__->isArray($next)) $next = str_split((string) $next);
+
+ $return = array_intersect($return, $next);
+ }
+
+ return self::_wrap(array_values($return));
+ }
+
+
+ // Merge together multiple arrays
+ public function union($array=null) {
+ $arrays = self::_wrapArgs(func_get_args(), 1);
+
+ if(count($arrays) === 1) return self::_wrap($array);
+
+ $__ = new self;
+ return self::_wrap($__->flatten(array_values(array_unique(call_user_func_array('array_merge', $arrays)))));
+ }
+
+
+ // Get the difference between two arrays
+ public function difference($array_one=null, $array_two=null) {
+ $arrays = self::_wrapArgs(func_get_args(), 1);
+
+ return self::_wrap(array_values(call_user_func_array('array_diff', $arrays)));
+ }
+
+
+ // Get the index of the first match
+ public function indexOf($collection=null, $item=null) {
+ list($collection, $item) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ $key = array_search($item, $collection, true);
+ return self::_wrap((is_bool($key)) ? -1 : $key);
+ }
+
+
+ // Get the index of the last match
+ public function lastIndexOf($collection=null, $item=null) {
+ list($collection, $item) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = self::_collection($collection);
+
+ krsort($collection);
+ $__ = new self;
+ return self::_wrap($__->indexOf($collection, $item));
+ }
+
+
+ // Returns an array of integers from start to stop (exclusive) by step
+ public function range($stop=null) {
+ $args = self::_wrapArgs(func_get_args(), 1);
+
+ $__ = new self;
+ $args = $__->reject($args, function($val) {
+ return is_null($val);
+ });
+
+ $num_args = count($args);
+ switch($num_args) {
+ case 1:
+ list($start, $stop, $step) = array(0, $args[0], 1);
+ break;
+ case 2:
+ list($start, $stop, $step) = array($args[0], $args[1], 1);
+ if($stop < $start) return self::_wrap(array());
+ break;
+ default:
+ list($start, $stop, $step) = array($args[0], $args[1], $args[2]);
+ if($step > 0 && $step > $stop) return self::_wrap(array($start));
+ }
+ $results = range($start, $stop, $step);
+
+ // Switch inclusive to exclusive
+ if($step > 0 && $__->last($results) >= $stop) array_pop($results);
+ elseif($step < 0 && $__->last($results) <= $stop) array_pop($results);
+
+ return self::_wrap($results);
+ }
+
+
+ // Merges arrays
+ public function zip($array=null) {
+ $arrays = self::_wrapArgs(func_get_args());
+ $num_arrays = count($arrays);
+ if($num_arrays === 1) return self::_wrap($array);
+
+ $__ = new self;
+ $num_return_arrays = $__->max($__->map($arrays, function($array) {
+ return count($array);
+ }));
+ $return_arrays = $__->range($num_return_arrays);
+ foreach($return_arrays as $k=>$v) {
+ if(!is_array($return_arrays[$k])) $return_arrays[$k] = array();
+
+ foreach($arrays as $a=>$array) {
+ $return_arrays[$k][$a] = array_key_exists($k, $array) ? $array[$k] : null;
+ }
+ }
+
+ return self::_wrap($return_arrays);
+ }
+
+
+ // Get the max value in the collection
+ public function max($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ if(is_null($iterator)) return self::_wrap(max($collection));
+
+ $results = array();
+ foreach($collection as $k=>$item) {
+ $results[$k] = $iterator($item);
+ }
+ arsort($results);
+ $__ = new self;
+ $first_key = $__->first(array_keys($results));
+ return $collection[$first_key];
+ }
+
+
+ // Get the min value in the collection
+ public function min($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ if(is_null($iterator)) return self::_wrap(min($collection));
+
+ $results = array();
+ foreach($collection as $k=>$item) {
+ $results[$k] = $iterator($item);
+ }
+ asort($results);
+ $__ = new self;
+ $first_key = $__->first(array_keys($results));
+ return self::_wrap($collection[$first_key]);
+ }
+
+
+ // Sort the collection by return values from the iterator
+ public function sortBy($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $results = array();
+ foreach($collection as $k=>$item) {
+ $results[$k] = $iterator($item);
+ }
+ asort($results);
+ foreach($results as $k=>$v) {
+ $results[$k] = $collection[$k];
+ }
+ return self::_wrap(array_values($results));
+ }
+
+
+ // Group the collection by return values from the iterator
+ public function groupBy($collection=null, $iterator=null) {
+ list($collection, $iterator) = self::_wrapArgs(func_get_args(), 2);
+
+ $result = array();
+ $collection = (array) $collection;
+ foreach($collection as $k=>$v) {
+ $key = (is_callable($iterator)) ? $iterator($v, $k) : $v[$iterator];
+ if(!array_key_exists($key, $result)) $result[$key] = array();
+ $result[$key][] = $v;
+ }
+ return $result;
+ }
+
+
+ // Returns the index at which the value should be inserted into the sorted collection
+ public function sortedIndex($collection=null, $value=null, $iterator=null) {
+ list($collection, $value, $iterator) = self::_wrapArgs(func_get_args(), 3);
+
+ $collection = (array) self::_collection($collection);
+ $__ = new self;
+
+ $calculated_value = (!is_null($iterator)) ? $iterator($value) : $value;
+
+ while(count($collection) > 1) {
+ $midpoint = floor(count($collection) / 2);
+ $midpoint_values = array_slice($collection, $midpoint, 1);
+ $midpoint_value = $midpoint_values[0];
+ $midpoint_calculated_value = (!is_null($iterator)) ? $iterator($midpoint_value) : $midpoint_value;
+
+ $collection = ($calculated_value < $midpoint_calculated_value) ? array_slice($collection, 0, $midpoint, true) : array_slice($collection, $midpoint, null, true);
+ }
+ $keys = array_keys($collection);
+
+ return self::_wrap(current($keys) + 1);
+ }
+
+ // Shuffle the array
+ public function shuffle($collection=null) {
+ list($collection) = self::_wrapArgs(func_get_args(), 1);
+
+ $collection = (array) self::_collection($collection);
+ shuffle($collection);
+
+ return self::_wrap($collection);
+ }
+
+
+ // Return the collection as an array
+ public function toArray($collection=null) {
+ return (array) $collection;
+ }
+
+
+ // Get the collection's keys
+ public function keys($collection=null) {
+ list($collection) = self::_wrapArgs(func_get_args(), 1);
+
+ if(!is_object($collection) && !is_array($collection)) throw new Exception('Invalid object');
+
+ return self::_wrap(array_keys((array) $collection));
+ }
+
+
+ // Get the collection's values
+ public function values($collection=null) {
+ list($collection) = self::_wrapArgs(func_get_args(), 1);
+
+ return self::_wrap(array_values((array) $collection));
+ }
+
+
+ // Copy all properties from the source objects into the destination object
+ public function extend($object=null) {
+ $args = self::_wrapArgs(func_get_args(), 1);
+
+ $num_args = func_num_args();
+ if($num_args === 1) return $object;
+
+ $is_object = is_object($object);
+ $array = (array) $object;
+ $__ = new self;
+ $extensions = $__->rest(func_get_args());
+ foreach($extensions as $extension) {
+ $extension = (array) $extension;
+ $array = array_merge($array, $extension);
+ }
+ return self::_wrap(($is_object) ? (object) $array : $array);
+ }
+
+
+ // Returns the object with any missing values filled in using the defaults.
+ public function defaults($object=null) {
+ $args = self::_wrapArgs(func_get_args(), 1);
+ list($object) = $args;
+
+ $num_args = count($args);
+ if($num_args === 1) return $object;
+
+ $is_object = is_object($object);
+ $array = (array) $object;
+ $__ = new self;
+ $extensions = $__->rest($args);
+ foreach($extensions as $extension) {
+ $extension = (array) $extension;
+ $array = array_merge($extension, $array);
+ }
+ return self::_wrap(($is_object) ? (object) $array : $array);
+ }
+
+
+ // Get the names of functions available to the object
+ // functions alias: methods
+ public function methods($object=null) { return self::functions($object); }
+ public function functions($object=null) {
+ list($object) = self::_wrapArgs(func_get_args(), 1);
+
+ return self::_wrap(get_class_methods(get_class($object)));
+ }
+
+
+ // Returns a shallow copy of the object
+ public function clon(&$object=null) {
+ list($object) = self::_wrapArgs(func_get_args(), 1);
+
+ $clone = null;
+ if(is_array($object)) $clone = (array) clone (object) $object;
+ elseif(!is_object($object)) $clone = $object;
+ elseif(!$clone) $clone = clone $object;
+
+ // shallow copy object
+ if(is_object($clone) && count($clone) > 0) {
+ foreach($clone as $k=>$v) {
+ if(is_array($v) || is_object($v)) $clone->$k =& $object->$k;
+ }
+ }
+
+ // shallow copy array
+ elseif(is_array($clone) && count($clone) > 0) {
+ foreach($clone as $k=>$v) {
+ if(is_array($v) || is_object($v)) $clone[$k] =& $object[$k];
+ }
+ }
+ return self::_wrap($clone);
+ }
+
+
+ // Invokes the interceptor on the object, then returns the object
+ public function tap($object=null, $interceptor=null) {
+ list($object, $interceptor) = self::_wrapArgs(func_get_args(), 2);
+
+ $interceptor($object);
+ return self::_wrap($object);
+ }
+
+
+ // Does the given key exist?
+ public function has($collection=null, $key=null) {
+ list($collection, $key) = self::_wrapArgs(func_get_args(), 2);
+
+ $collection = (array) self::_collection($collection);
+
+ return self::_wrap(array_key_exists($key, $collection));
+ }
+
+
+ // Are these items equal?
+ public function isEqual($a=null, $b=null) {
+ list($a, $b) = self::_wrapArgs(func_get_args(), 2);
+
+ if(isset($this) && isset($this->_chained) && $this->_chained) $a =& $this;
+
+ if($a === $b) return self::_wrap(true);
+ if(gettype($a) !== gettype($b)) return self::_wrap(false);
+ if(is_callable($a) !== is_callable($b)) return self::_wrap(false);
+
+ if($a == $b) return self::_wrap(true);
+
+ // Objects and arrays compared by values
+ if(is_object($a) || is_array($a)) {
+
+ // Do either implement isEqual()?
+ if(is_object($a) && isset($a->isEqual)) return self::_wrap($a->isEqual($b));
+ if(is_object($b) && isset($b->isEqual)) return self::_wrap($b->isEqual($a));
+ if(is_array($a) && array_key_exists('isEqual', $a)) return self::_wrap($a['isEqual']($b));
+ if(is_array($b) && array_key_exists('isEqual', $b)) return self::_wrap($b['isEqual']($a));
+
+ if(count($a) !== count($b)) return self::_wrap(false);
+
+ $__ = new self;
+ $keys_equal = $__->isEqual($__->keys($a), $__->keys($b));
+ $values_equal = $__->isEqual($__->values($a), $__->values($b));
+ return self::_wrap($keys_equal && $values_equal);
+ }
+
+ return self::_wrap(false);
+ }
+
+
+ // Is this item empty?
+ public function isEmpty($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+
+ return self::_wrap(is_array($item) || is_object($item)) ? !((bool) count((array) $item)) : (!(bool) $item);
+ }
+
+
+ // Is this item an object?
+ public function isObject($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_object($item));
+ }
+
+
+ // Is this item an array?
+ public function isArray($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_array($item));
+ }
+
+
+ // Is this item a string?
+ public function isString($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_string($item));
+ }
+
+
+ // Is this item a number?
+ public function isNumber($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap((is_int($item) || is_float($item)) && !is_nan($item) && !is_infinite($item));
+ }
+
+
+ // Is this item a bool?
+ public function isBoolean($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_bool($item));
+ }
+
+
+ // Is this item a function (by type, not by name)?
+ public function isFunction($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_object($item) && is_callable($item));
+ }
+
+
+ // Is this item an instance of DateTime?
+ public function isDate($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_object($item) && get_class($item) === 'DateTime');
+ }
+
+
+ // Is this item a NaN value?
+ public function isNaN($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+ return self::_wrap(is_nan($item));
+ }
+
+
+ // Returns the same value passed as the argument
+ public function identity() {
+ $args = self::_wrapArgs(func_get_args(), 1);
+
+ if(is_array($args)) return self::_wrap($args[0]);
+
+ return self::_wrap(function($x) {
+ return $x;
+ });
+ }
+
+
+ // Generate a globally unique id, optionally prefixed
+ public $_uniqueId = -1;
+ public function uniqueId($prefix=null) {
+ list($prefix) = self::_wrapArgs(func_get_args(), 1);
+
+ $_instance = self::getInstance();
+ $_instance->_uniqueId++;
+
+ return (is_null($prefix)) ? self::_wrap($_instance->_uniqueId) : self::_wrap($prefix . $_instance->_uniqueId);
+ }
+
+
+ // Invokes the iterator n times
+ public function times($n=null, $iterator=null) {
+ list($n, $iterator) = self::_wrapArgs(func_get_args(), 2);
+ if(is_null($n)) $n = 0;
+
+ for($i=0; $i<$n; $i++) $iterator($i);
+ return self::_wrap(null);
+ }
+
+
+ // Extend the class with your own functions
+ private $_mixins = array();
+ public function mixin($functions=null) {
+ list($functions) = self::_wrapArgs(func_get_args(), 1);
+
+ $mixins =& self::getInstance()->_mixins;
+ foreach($functions as $name=>$function) {
+ $mixins[$name] = $function;
+ }
+ return self::_wrap(null);
+ }
+
+
+ // Allows extending methods in static context
+ public static function __callStatic($name, $arguments) {
+ $mixins =& self::getInstance()->_mixins;
+ return call_user_func_array($mixins[$name], $arguments);
+ }
+
+ // Allows extending methods in non-static context
+ public function __call($name, $arguments) {
+ $mixins =& self::getInstance()->_mixins;
+ $arguments = self::_wrapArgs($arguments);
+ return call_user_func_array($mixins[$name], $arguments);
+ }
+
+
+ // Temporary PHP open and close tags used within templates
+ // Allows for normal processing of templates even when
+ // the developer uses PHP open or close tags for interpolation or evaluation
+ const TEMPLATE_OPEN_TAG = '760e7dab2836853c63805033e514668301fa9c47';
+ const TEMPLATE_CLOSE_TAG= 'd228a8fa36bd7db108b01eddfb03a30899987a2b';
+
+ const TEMPLATE_DEFAULT_EVALUATE = '/<%([\s\S]+?)%>/';
+ const TEMPLATE_DEFAULT_INTERPOLATE= '/<%=([\s\S]+?)%>/';
+ const TEMPLATE_DEFAULT_ESCAPE = '/<%-([\s\S]+?)%>/';
+ public $_template_settings = array(
+ 'evaluate' => self::TEMPLATE_DEFAULT_EVALUATE,
+ 'interpolate' => self::TEMPLATE_DEFAULT_INTERPOLATE,
+ 'escape' => self::TEMPLATE_DEFAULT_ESCAPE
+ );
+
+ // Set template settings
+ public function templateSettings($settings=null) {
+ $_template_settings =& self::getInstance()->_template_settings;
+
+ if(is_null($settings)) {
+ $_template_settings = array(
+ 'evaluate' => self::TEMPLATE_DEFAULT_EVALUATE,
+ 'interpolate' => self::TEMPLATE_DEFAULT_INTERPOLATE,
+ 'escape' => self::TEMPLATE_DEFAULT_ESCAPE
+ );
+ return true;
+ }
+
+ foreach($settings as $k=>$v) {
+ if(!array_key_exists($k, $_template_settings)) continue;
+
+ $_template_settings[$k] = $v;
+ }
+ return true;
+ }
+
+
+ // Compile templates into functions that can be evaluated for rendering
+ public function template($code=null, $context=null) {
+ list($code, $context) = self::_wrapArgs(func_get_args(), 2);
+
+ $class_name = __CLASS__;
+
+ $return = self::_wrap(function($context=null) use ($code, $class_name) {
+ $ts = $class_name::getInstance()->_template_settings;
+
+ // Wrap escaped, interpolated, and evaluated blocks inside PHP tags
+ extract((array) $context);
+ preg_match_all($ts['escape'], $code, $vars, PREG_SET_ORDER);
+ if(count($vars) > 0) {
+ foreach($vars as $var) {
+ $echo = $class_name::TEMPLATE_OPEN_TAG . ' echo htmlentities(' . trim($var[1]) . '); ' . $class_name::TEMPLATE_CLOSE_TAG;
+ $code = str_replace($var[0], $echo, $code);
+ }
+ }
+ preg_match_all($ts['interpolate'], $code, $vars, PREG_SET_ORDER);
+ if(count($vars) > 0) {
+ foreach($vars as $var) {
+ $echo = $class_name::TEMPLATE_OPEN_TAG . ' echo ' . trim($var[1]) . '; ' . $class_name::TEMPLATE_CLOSE_TAG;
+ $code = str_replace($var[0], $echo, $code);
+ }
+ }
+ preg_match_all($ts['evaluate'], $code, $vars, PREG_SET_ORDER);
+ if(count($vars) > 0) {
+ foreach($vars as $var) {
+ $echo = $class_name::TEMPLATE_OPEN_TAG . trim($var[1]) . $class_name::TEMPLATE_CLOSE_TAG;
+ $code = str_replace($var[0], $echo, $code);
+ }
+ }
+ $code = str_replace($class_name::TEMPLATE_OPEN_TAG, '', $code);
+
+ // Use the output buffer to grab the return value
+ $code = 'ob_start(); extract($context); ?>' . $code . '_wrapped) && $this->_wrapped) || !is_null($context)) ? $return($context) : $return);
+ }
+
+ // Escape
+ public function escape($item=null) {
+ list($item) = self::_wrapArgs(func_get_args(), 1);
+
+ return self::_wrap(htmlentities($item));
+ }
+
+
+ // Memoizes a function by caching the computed result.
+ public $_memoized = array();
+ public function memoize($function=null, $hashFunction=null) {
+ list($function, $hashFunction) = self::_wrapArgs(func_get_args(), 2);
+
+ $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
+
+ return self::_wrap(function() use ($function, &$_instance, $hashFunction) {
+
+ // Generate a key based on hashFunction
+ $args = func_get_args();
+ if(is_null($hashFunction)) $hashFunction = function($function, $args) {
+
+ // Try using var_export to identify the function
+ return md5(join('_', array(
+ var_export($function, true),
+ var_export($args, true)
+ )));
+ };
+ $key = $hashFunction($function, $args);
+
+ if(!array_key_exists($key, $_instance->_memoized)) {
+ $_instance->_memoized[$key] = call_user_func_array($function, $args);
+ }
+ return $_instance->_memoized[$key];
+ });
+ }
+
+
+ // Throttles a function so that it can only be called once every wait milliseconds
+ public $_throttled = array();
+ public function throttle($function=null, $wait=null) {
+ list($function, $wait) = self::_wrapArgs(func_get_args(), 2);
+
+ $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
+
+ return self::_wrap(function() use ($function, $wait, &$_instance) {
+
+ // Try using var_export to identify the function
+ $key = md5(join('', array(
+ var_export($function, true),
+ $wait
+ )));
+
+ $microtime = microtime(true);
+ $ready_to_call = (!array_key_exists($key, $_instance->_throttled) || $microtime >= $_instance->_throttled[$key]);
+ if($ready_to_call) {
+ $next_callable_time = $microtime + ($wait / 1000);
+ $_instance->_throttled[$key] = $next_callable_time;
+ return call_user_func_array($function, func_get_args());
+ }
+ });
+ }
+
+
+ // Creates a version of the function that can only be called once
+ public $_onced = array();
+ public function once($function=null) {
+ list($function) = self::_wrapArgs(func_get_args(), 1);
+
+ $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
+
+ return self::_wrap(function() use ($function, &$_instance) {
+
+ // Try using var_export to identify the function
+ $key = md5(var_export($function, true));
+ if(!array_key_exists($key, $_instance->_onced)) {
+ $_instance->_onced[$key] = call_user_func_array($function, func_get_args());
+ }
+
+ return $_instance->_onced[$key];
+ });
+ }
+
+
+ // Wraps the function inside the wrapper function, passing it as the first argument
+ public function wrap($function=null, $wrapper=null) {
+ list($function, $wrapper) = self::_wrapArgs(func_get_args(), 2);
+
+ return self::_wrap(function() use ($wrapper, $function) {
+ $args = array_merge(array($function), func_get_args());
+ return call_user_func_array($wrapper, $args);
+ });
+ }
+
+
+ // Returns the composition of the functions
+ public function compose() {
+ $functions = self::_wrapArgs(func_get_args(), 1);
+
+ return self::_wrap(function() use ($functions) {
+ $args = func_get_args();
+ foreach($functions as $function) {
+ $args[0] = call_user_func_array($function, $args);
+ }
+ return $args[0];
+ });
+ }
+
+
+ // Creates a version of the function that will only run after being called count times
+ public $_aftered = array();
+ public function after($count=null, $function=null) {
+ list($count, $function) = self::_wrapArgs(func_get_args(), 2);
+
+ $_instance = (isset($this) && isset($this->_wrapped)) ? $this : self::getInstance();
+ $key = md5(mt_rand());
+
+ $func = function() use ($function, &$_instance, $count, $key) {
+ if(!array_key_exists($key, $_instance->_aftered)) $_instance->_aftered[$key] = 0;
+ $_instance->_aftered[$key] += 1;
+
+ if($_instance->_aftered[$key] >= $count) return call_user_func_array($function, func_get_args());
+ };
+ return self::_wrap(($count) ? $func : $func());
+ }
+
+
+ // Singleton
+ private static $_instance;
+ public function getInstance() {
+ if(!isset(self::$_instance)) {
+ $c = __CLASS__;
+ self::$_instance = new $c;
+ }
+ return self::$_instance;
+ }
+
+
+ // All methods should wrap their returns within _wrap
+ // because this function understands both OO-style and functional calls
+ public $_wrapped; // Value passed from one chained method to the next
+ private function _wrap($val) {
+ if(isset($this) && isset($this->_chained) && $this->_chained) {
+ $this->_wrapped = $val;
+ return $this;
+ }
+ return $val;
+ }
+
+
+ // All methods should get their arguments from _wrapArgs
+ // because this function understands both OO-style and functional calls
+ private function _wrapArgs($caller_args, $num_args=null) {
+ $num_args = (is_null($num_args)) ? count($caller_args) - 1 : $num_args;
+
+ $filled_args = array();
+ if(isset($this) && isset($this->_wrapped)) {
+ $filled_args[] =& $this->_wrapped;
+ }
+ if(count($caller_args) > 0) {
+ foreach($caller_args as $k=>$v) {
+ $filled_args[] = $v;
+ }
+ }
+
+ return array_pad($filled_args, $num_args, null);
+ }
+
+
+ // Get a collection in a way that supports strings
+ private function _collection($collection) {
+ return (!is_array($collection) && !is_object($collection)) ? str_split((string) $collection) : $collection;
+ }
+}
diff --git a/php/underscoremin.php b/php/underscoremin.php
new file mode 100644
index 0000000..e69de29