Ticket #1023: new-dependencies.dpatch

File new-dependencies.dpatch, 44.6 KB (added by josipl, at 2010-08-01T00:26:46Z)

Includes new dependencies (notifications library and Last.fm API wrapper) and a fix for MooTools?' JSON parser.

Line 
11 patch for repository /home/josip/bin/tahoe-tmp:
2
3Mon Jul 19 14:18:52 CEST 2010  josip.lisec@gmail.com
4  * add-new-deps
5    * Fixed bug in MooTools' JSON implementation
6    * Added Last.fm API wrapper
7    * Added notifications library (Roar)
8
9New patches:
10
11[add-new-deps
12josip.lisec@gmail.com**20100719121852
13 Ignore-this: a35acadac15279b65bd25dcd89a8dbef
14   * Fixed bug in MooTools' JSON implementation
15   * Added Last.fm API wrapper
16   * Added notifications library (Roar)
17] {
18hunk ./contrib/musicplayer/src/libs/vendor/JSON.js 1
19-(function () {
20-/**
21- * da.vendor.JSON
22- *
23- * JSON parser/serializer.
24- **/
25-
26-var has_JSON = typeof JSON === "undefined",
27-    broken_JSON = false;
28-
29-if(has_JSON && JSON.stringify([1,2,3]) === "[1,2,3]")
30-  broken_JSON = true;
31
32-if(!has_JSON || broken_JSON) {
33-//#require "libs/vendor/json/json2.js"
34-
35-}
36-})();
37rmfile ./contrib/musicplayer/src/libs/vendor/JSON.js
38adddir ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api
39addfile ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/README
40hunk ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/README 1
41+JavaScript last.fm API Readme
42+-----------------------------
43+
44+Overview
45+--------
46+
47+The JavaScript last.fm API allows you to call last.fm API methods and get the
48+corresponding JSON responses. Basically it just acts as a wrapper that gives
49+easy access to API methods. Responses can be cached using the localStorage API.
50+
51+
52+Encoding
53+--------
54+
55+You don't need to worry about encoding when calling API methods. Everything
56+should automatically be UTF-8 encoded and decoded by your browser if you set
57+the Content-Type for your document to UTF-8:
58+
59+       <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
60+
61+
62+Authentication
63+--------------
64+
65+It's easy to fetch a session for a user account. This allows you to perform
66+actions on that account in a manner that is secure for last.fm users. All
67+write services require authentication.
68+
69+
70+Write methods
71+-------------
72+
73+Due to technical restrictions it's not possible to get a response when calling
74+write methods. Reading responses is only possible using HTTP Access-Control,
75+which is currently not supported by the last.fm API.
76+
77+
78+Submissions
79+-----------
80+
81+Scrobbling is not yet supported.
82+
83+
84+Usage
85+-----
86+
87+You need to add the following scripts to your code in order to work with the
88+JavaScript last.fm API:
89+
90+       <script type="text/javascript" src="lastfm.api.md5.js"></script>
91+       <script type="text/javascript" src="lastfm.api.js"></script>
92+
93+If you want to use caching, you need to add another script:
94+
95+       <script type="text/javascript" src="lastfm.api.cache.js"></script>
96+
97+Built in JSON support should be available in modern browsers, if you want to
98+be sure it's supported, include the following:
99+
100+       <script type="text/javascript" src="json2.js"></script>
101+
102+You can get that file at http://www.json.org/json2.js , and make sure you've
103+removed the first line before using it.
104+
105+Now you can use the JavaScript last.fm API like this:
106+
107+       /* Create a cache object */
108+       var cache = new LastFMCache();
109+
110+       /* Create a LastFM object */
111+       var lastfm = new LastFM({
112+               apiKey    : 'f21088bf9097b49ad4e7f487abab981e',
113+               apiSecret : '7ccaec2093e33cded282ec7bc81c6fca',
114+               cache     : cache
115+       });
116+
117+       /* Load some artist info. */
118+       lastfm.artist.getInfo({artist: 'The xx'}, {success: function(data){
119+               /* Use data. */
120+       }, error: function(code, message){
121+               /* Show error message. */
122+       }});
123+
124+
125+More
126+----
127+
128+For further information, please visit the official API documentation:
129+http://www.last.fm/api
130addfile ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/lastfm.api.cache.js
131hunk ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/lastfm.api.cache.js 1
132+/*
133+ *
134+ * Copyright (c) 2008-2009, Felix Bruns <felixbruns@web.de>
135+ *
136+ */
137+
138+/* Set an object on a Storage object. */
139+Storage.prototype.setObject = function(key, value){
140+       this.setItem(key, JSON.stringify(value));
141+}
142+
143+/* Get an object from a Storage object. */
144+Storage.prototype.getObject = function(key){
145+       var item = this.getItem(key);
146+
147+       return JSON.parse(item);
148+}
149+
150+/* Creates a new cache object. */
151+function LastFMCache(){
152+       /* Expiration times. */
153+       var MINUTE =          60;
154+       var HOUR   = MINUTE * 60;
155+       var DAY    = HOUR   * 24;
156+       var WEEK   = DAY    *  7;
157+       var MONTH  = WEEK   *  4.34812141;
158+       var YEAR   = MONTH  * 12;
159+
160+       /* Methods with weekly expiration. */
161+       var weeklyMethods = [
162+               'artist.getSimilar',
163+               'tag.getSimilar',
164+               'track.getSimilar',
165+               'artist.getTopAlbums',
166+               'artist.getTopTracks',
167+               'geo.getTopArtists',
168+               'geo.getTopTracks',
169+               'tag.getTopAlbums',
170+               'tag.getTopArtists',
171+               'tag.getTopTags',
172+               'tag.getTopTracks',
173+               'user.getTopAlbums',
174+               'user.getTopArtists',
175+               'user.getTopTags',
176+               'user.getTopTracks'
177+       ];
178+
179+       /* Name for this cache. */
180+       var name = 'lastfm';
181+
182+       /* Create cache if it doesn't exist yet. */
183+       if(localStorage.getObject(name) == null){
184+               localStorage.setObject(name, []);
185+       }
186+
187+       /* Get expiration time for given parameters. */
188+       this.getExpirationTime = function(params){
189+               var method = params.method;
190+
191+               if((/Weekly/).test(method) && !(/List/).test(method)){
192+                       if(typeof(params.to) != 'undefined' && typeof(params.from) != 'undefined'){
193+                               return YEAR;
194+                       }
195+                       else{
196+                               return WEEK;
197+                       }
198+               }
199+
200+               for(var key in this.weeklyMethods){
201+                       if(method == this.weeklyMethods[key]){
202+                               return WEEK;
203+                       }
204+               }
205+
206+               return -1;
207+       };
208+
209+       /* Check if this cache contains specific data. */
210+       this.contains = function(hash){
211+               return typeof(localStorage.getObject(name)[hash]) != 'undefined' &&
212+                       typeof(localStorage.getObject(name)[hash].data) != 'undefined';
213+       };
214+
215+       /* Load data from this cache. */
216+       this.load = function(hash){
217+               return localStorage.getObject(name)[hash].data;
218+       };
219+
220+       /* Remove data from this cache. */
221+       this.remove = function(hash){
222+               var object = localStorage.getObject(name);
223+
224+               object[hash] = undefined;
225+
226+               localStorage.setObject(name, object);
227+       };
228+
229+       /* Store data in this cache with a given expiration time. */
230+       this.store = function(hash, data, expiration){
231+               var object = localStorage.getObject(name);
232+               var time   = Math.round(new Date().getTime() / 1000);
233+
234+               object[hash] = {
235+                       data       : data,
236+                       expiration : time + expiration
237+               };
238+
239+               localStorage.setObject(name, object);
240+       };
241+
242+       /* Check if some specific data expired. */
243+       this.isExpired = function(hash){
244+               var object = localStorage.getObject(name);
245+               var time   = Math.round(new Date().getTime() / 1000);
246+
247+               if(time > object[hash].expiration){
248+                       return true;
249+               }
250+
251+               return false;
252+       };
253+
254+       /* Clear this cache. */
255+       this.clear = function(){
256+               localStorage.setObject(name, []);
257+       };
258+};
259addfile ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/lastfm.api.js
260hunk ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/lastfm.api.js 1
261+/*
262+ *
263+ * Copyright (c) 2008-2010, Felix Bruns <felixbruns@web.de>
264+ *
265+ */
266+
267+function LastFM(options){
268+       /* Set default values for required options. */
269+       var apiKey    = options.apiKey    || '';
270+       var apiSecret = options.apiSecret || '';
271+       var apiUrl    = options.apiUrl    || 'http://ws.audioscrobbler.com/2.0/';
272+       var cache     = options.cache     || undefined;
273+
274+       /* Set API key. */
275+       this.setApiKey = function(_apiKey){
276+               apiKey = _apiKey;
277+       };
278+
279+       /* Set API key. */
280+       this.setApiSecret = function(_apiSecret){
281+               apiSecret = _apiSecret;
282+       };
283+
284+       /* Set API URL. */
285+       this.setApiUrl = function(_apiUrl){
286+               apiUrl = _apiUrl;
287+       };
288+
289+       /* Set cache. */
290+       this.setCache = function(_cache){
291+               cache = _cache;
292+       };
293+
294+       /* Internal call (POST, GET). */
295+       var internalCall = function(params, callbacks, requestMethod){
296+               /* Cross-domain POST request (doesn't return any data, always successful). */
297+               if(requestMethod == 'POST'){
298+                       /* Create iframe element to post data. */
299+                       var html   = document.getElementsByTagName('html')[0];
300+                       var iframe = document.createElement('iframe');
301+                       var doc;
302+
303+                       /* Set iframe attributes. */
304+                       iframe.width        = 1;
305+                       iframe.height       = 1;
306+                       iframe.style.border = 'none';
307+                       iframe.onload       = function(){
308+                               /* Remove iframe element. */
309+                               //html.removeChild(iframe);
310+
311+                               /* Call user callback. */
312+                               if(typeof(callbacks.success) != 'undefined'){
313+                                       callbacks.success();
314+                               }
315+                       };
316+
317+                       /* Append iframe. */
318+                       html.appendChild(iframe);
319+
320+                       /* Get iframe document. */
321+                       if(typeof(iframe.contentWindow) != 'undefined'){
322+                               doc = iframe.contentWindow.document;
323+                       }
324+                       else if(typeof(iframe.contentDocument.document) != 'undefined'){
325+                               doc = iframe.contentDocument.document.document;
326+                       }
327+                       else{
328+                               doc = iframe.contentDocument.document;
329+                       }
330+
331+                       /* Open iframe document and write a form. */
332+                       doc.open();
333+                       doc.clear();
334+                       doc.write('<form method="post" action="' + apiUrl + '" id="form">');
335+
336+                       /* Write POST parameters as input fields. */
337+                       for(var param in params){
338+                               doc.write('<input type="text" name="' + param + '" value="' + params[param] + '">');
339+                       }
340+
341+                       /* Write automatic form submission code. */
342+                       doc.write('</form>');
343+                       doc.write('<script type="application/x-javascript">');
344+                       doc.write('document.getElementById("form").submit();');
345+                       doc.write('</script>');
346+
347+                       /* Close iframe document. */
348+                       doc.close();
349+               }
350+               /* Cross-domain GET request (JSONP). */
351+               else{
352+                       /* Get JSONP callback name. */
353+                       var jsonp = 'jsonp' + new Date().getTime();
354+
355+                       /* Calculate cache hash. */
356+                       var hash = auth.getApiSignature(params);
357+
358+                       /* Check cache. */
359+                       if(typeof(cache) != 'undefined' && cache.contains(hash) && !cache.isExpired(hash)){
360+                               if(typeof(callbacks.success) != 'undefined'){
361+                                       callbacks.success(cache.load(hash));
362+                               }
363+
364+                               return;
365+                       }
366+
367+                       /* Set callback name and response format. */
368+                       params.callback = jsonp;
369+                       params.format   = 'json';
370+
371+                       /* Create JSONP callback function. */
372+                       window[jsonp] = function(data){
373+                               /* Is a cache available?. */
374+                               if(typeof(cache) != 'undefined'){
375+                                       var expiration = cache.getExpirationTime(params);
376+
377+                                       if(expiration > 0){
378+                                               cache.store(hash, data, expiration);
379+                                       }
380+                               }
381+
382+                               /* Call user callback. */
383+                               if(typeof(data.error) != 'undefined'){
384+                                       if(typeof(callbacks.error) != 'undefined'){
385+                                               callbacks.error(data.error, data.message);
386+                                       }
387+                               }
388+                               else if(typeof(callbacks.success) != 'undefined'){
389+                                       callbacks.success(data);
390+                               }
391+
392+                               /* Garbage collect. */
393+                               window[jsonp] = undefined;
394+
395+                               try{
396+                                       delete window[jsonp];
397+                               }
398+                               catch(e){
399+                                       /* Nothing. */
400+                               }
401+
402+                               /* Remove script element. */
403+                               if(head){
404+                                       head.removeChild(script);
405+                               }
406+                       };
407+
408+                       /* Create script element to load JSON data. */
409+                       var head   = document.getElementsByTagName("head")[0];
410+                       var script = document.createElement("script");
411+
412+                       /* Build parameter string. */
413+                       var array = [];
414+
415+                       for(var param in params){
416+                               array.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
417+                       }
418+
419+                       /* Set script source. */
420+                       script.src = apiUrl + '?' + array.join('&').replace(/%20/g, '+');
421+
422+                       /* Append script element. */
423+                       head.appendChild(script);
424+               }
425+       };
426+
427+       /* Normal method call. */
428+       var call = function(method, params, callbacks, requestMethod){
429+               /* Set default values. */
430+               params        = params        || {};
431+               callbacks     = callbacks     || {};
432+               requestMethod = requestMethod || 'GET';
433+
434+               /* Add parameters. */
435+               params.method  = method;
436+               params.api_key = apiKey;
437+
438+               /* Call method. */
439+               internalCall(params, callbacks, requestMethod);
440+       };
441+
442+       /* Signed method call. */
443+       var signedCall = function(method, params, session, callbacks, requestMethod){
444+               /* Set default values. */
445+               params        = params        || {};
446+               callbacks     = callbacks     || {};
447+               requestMethod = requestMethod || 'GET';
448+
449+               /* Add parameters. */
450+               params.method  = method;
451+               params.api_key = apiKey;
452+
453+               /* Add session key. */
454+               if(session && typeof(session.key) != 'undefined'){
455+                       params.sk = session.key;
456+               }
457+
458+               /* Get API signature. */
459+               params.api_sig = auth.getApiSignature(params);
460+
461+               /* Call method. */
462+               internalCall(params, callbacks, requestMethod);
463+       };
464+
465+       /* Album methods. */
466+       this.album = {
467+               addTags : function(params, session, callbacks){
468+                       /* Build comma separated tags string. */
469+                       if(typeof(params.tags) == 'object'){
470+                               params.tags = params.tags.join(',');
471+                       }
472+
473+                       signedCall('album.addTags', params, session, callbacks, 'POST');
474+               },
475+
476+               getBuylinks : function(params, callbacks){
477+                       call('album.getBuylinks', params, callbacks);
478+               },
479+
480+               getInfo : function(params, callbacks){
481+                       call('album.getInfo', params, callbacks);
482+               },
483+
484+               getTags : function(params, session, callbacks){
485+                       signedCall('album.getTags', params, session, callbacks);
486+               },
487+
488+               removeTag : function(params, session, callbacks){
489+                       signedCall('album.removeTag', params, session, callbacks, 'POST');
490+               },
491+
492+               search : function(params, callbacks){
493+                       call('album.search', params, callbacks);
494+               },
495+
496+               share : function(params, session, callbacks){
497+                       /* Build comma separated recipients string. */
498+                       if(typeof(params.recipient) == 'object'){
499+                               params.recipient = params.recipient.join(',');
500+                       }
501+
502+                       signedCall('album.share', params, callbacks);
503+               }
504+       };
505+
506+       /* Artist methods. */
507+       this.artist = {
508+               addTags : function(params, session, callbacks){
509+                       /* Build comma separated tags string. */
510+                       if(typeof(params.tags) == 'object'){
511+                               params.tags = params.tags.join(',');
512+                       }
513+
514+                       signedCall('artist.addTags', params, session, callbacks, 'POST');
515+               },
516+
517+               getEvents : function(params, callbacks){
518+                       call('artist.getEvents', params, callbacks);
519+               },
520+
521+               getImages : function(params, callbacks){
522+                       call('artist.getImages', params, callbacks);
523+               },
524+
525+               getInfo : function(params, callbacks){
526+                       call('artist.getInfo', params, callbacks);
527+               },
528+
529+               getPastEvents : function(params, callbacks){
530+                       call('artist.getPastEvents', params, callbacks);
531+               },
532+
533+               getPodcast : function(params, callbacks){
534+                       call('artist.getPodcast', params, callbacks);
535+               },
536+
537+               getShouts : function(params, callbacks){
538+                       call('artist.getShouts', params, callbacks);
539+               },
540+
541+               getSimilar : function(params, callbacks){
542+                       call('artist.getSimilar', params, callbacks);
543+               },
544+
545+               getTags : function(params, session, callbacks){
546+                       signedCall('artist.getTags', params, session, callbacks);
547+               },
548+
549+               getTopAlbums : function(params, callbacks){
550+                       call('artist.getTopAlbums', params, callbacks);
551+               },
552+
553+               getTopFans : function(params, callbacks){
554+                       call('artist.getTopFans', params, callbacks);
555+               },
556+
557+               getTopTags : function(params, callbacks){
558+                       call('artist.getTopTags', params, callbacks);
559+               },
560+
561+               getTopTracks : function(params, callbacks){
562+                       call('artist.getTopTracks', params, callbacks);
563+               },
564+
565+               removeTag : function(params, session, callbacks){
566+                       signedCall('artist.removeTag', params, session, callbacks, 'POST');
567+               },
568+
569+               search : function(params, callbacks){
570+                       call('artist.search', params, callbacks);
571+               },
572+
573+               share : function(params, session, callbacks){
574+                       /* Build comma separated recipients string. */
575+                       if(typeof(params.recipient) == 'object'){
576+                               params.recipient = params.recipient.join(',');
577+                       }
578+
579+                       signedCall('artist.share', params, session, callbacks, 'POST');
580+               },
581+
582+               shout : function(params, session, callbacks){
583+                       signedCall('artist.shout', params, session, callbacks, 'POST');
584+               }
585+       };
586+
587+       /* Auth methods. */
588+       this.auth = {
589+               getMobileSession : function(params, callbacks){
590+                       /* Set new params object with authToken. */
591+                       params = {
592+                               username  : params.username,
593+                               authToken : md5(params.username + md5(params.password))
594+                       };
595+
596+                       signedCall('auth.getMobileSession', params, null, callbacks);
597+               },
598+
599+               getSession : function(params, callbacks){
600+                       signedCall('auth.getSession', params, null, callbacks);
601+               },
602+
603+               getToken : function(callbacks){
604+                       signedCall('auth.getToken', null, null, callbacks);
605+               },
606+
607+               /* Deprecated. Security hole was fixed. */
608+               getWebSession : function(callbacks){
609+                       /* Save API URL and set new one (needs to be done due to a cookie!). */
610+                       var previuousApiUrl = apiUrl;
611+
612+                       apiUrl = 'http://ext.last.fm/2.0/';
613+
614+                       signedCall('auth.getWebSession', null, null, callbacks);
615+
616+                       /* Restore API URL. */
617+                       apiUrl = previuousApiUrl;
618+               }
619+       };
620+
621+       /* Event methods. */
622+       this.event = {
623+               attend : function(params, session, callbacks){
624+                       signedCall('event.attend', params, session, callbacks, 'POST');
625+               },
626+
627+               getAttendees : function(params, session, callbacks){
628+                       call('event.getAttendees', params, callbacks);
629+               },
630+
631+               getInfo : function(params, callbacks){
632+                       call('event.getInfo', params, callbacks);
633+               },
634+
635+               getShouts : function(params, callbacks){
636+                       call('event.getShouts', params, callbacks);
637+               },
638+
639+               share : function(params, session, callbacks){
640+                       /* Build comma separated recipients string. */
641+                       if(typeof(params.recipient) == 'object'){
642+                               params.recipient = params.recipient.join(',');
643+                       }
644+
645+                       signedCall('event.share', params, session, callbacks, 'POST');
646+               },
647+
648+               shout : function(params, session, callbacks){
649+                       signedCall('event.shout', params, session, callbacks, 'POST');
650+               }
651+       };
652+
653+       /* Geo methods. */
654+       this.geo = {
655+               getEvents : function(params, callbacks){
656+                       call('geo.getEvents', params, callbacks);
657+               },
658+
659+               getMetroArtistChart : function(params, callbacks){
660+                       call('geo.getMetroArtistChart', params, callbacks);
661+               },
662+
663+               getMetroHypeArtistChart : function(params, callbacks){
664+                       call('geo.getMetroHypeArtistChart', params, callbacks);
665+               },
666+
667+               getMetroHypeTrackChart : function(params, callbacks){
668+                       call('geo.getMetroHypeTrackChart', params, callbacks);
669+               },
670+
671+               getMetroTrackChart : function(params, callbacks){
672+                       call('geo.getMetroTrackChart', params, callbacks);
673+               },
674+
675+               getMetroUniqueArtistChart : function(params, callbacks){
676+                       call('geo.getMetroUniqueArtistChart', params, callbacks);
677+               },
678+
679+               getMetroUniqueTrackChart : function(params, callbacks){
680+                       call('geo.getMetroUniqueTrackChart', params, callbacks);
681+               },
682+
683+               getMetroWeeklyChartlist : function(params, callbacks){
684+                       call('geo.getMetroWeeklyChartlist', params, callbacks);
685+               },
686+
687+               getTopArtists : function(params, callbacks){
688+                       call('geo.getTopArtists', params, callbacks);
689+               },
690+
691+               getTopTracks : function(params, callbacks){
692+                       call('geo.getTopTracks', params, callbacks);
693+               }
694+       };
695+
696+       /* Group methods. */
697+       this.group = {
698+               getMembers : function(params, callbacks){
699+                       call('group.getMembers', params, callbacks);
700+               },
701+
702+               getWeeklyAlbumChart : function(params, callbacks){
703+                       call('group.getWeeklyAlbumChart', params, callbacks);
704+               },
705+
706+               getWeeklyArtistChart : function(params, callbacks){
707+                       call('group.group.getWeeklyArtistChart', params, callbacks);
708+               },
709+
710+               getWeeklyChartList : function(params, callbacks){
711+                       call('group.getWeeklyChartList', params, callbacks);
712+               },
713+
714+               getWeeklyTrackChart : function(params, callbacks){
715+                       call('group.getWeeklyTrackChart', params, callbacks);
716+               }
717+       };
718+
719+       /* Library methods. */
720+       this.library = {
721+               addAlbum : function(params, session, callbacks){
722+                       signedCall('library.addAlbum', params, session, callbacks, 'POST');
723+               },
724+
725+               addArtist : function(params, session, callbacks){
726+                       signedCall('library.addArtist', params, session, callbacks, 'POST');
727+               },
728+
729+               addTrack : function(params, session, callbacks){
730+                       signedCall('library.addTrack', params, session, callbacks, 'POST');
731+               },
732+
733+               getAlbums : function(params, callbacks){
734+                       call('library.getAlbums', params, callbacks);
735+               },
736+
737+               getArtists : function(params, callbacks){
738+                       call('library.getArtists', params, callbacks);
739+               },
740+
741+               getTracks : function(params, callbacks){
742+                       call('library.getTracks', params, callbacks);
743+               }
744+       };
745+
746+       /* Playlist methods. */
747+       this.playlist = {
748+               addTrack : function(params, session, callbacks){
749+                       signedCall('playlist.addTrack', params, session, callbacks, 'POST');
750+               },
751+
752+               create : function(params, session, callbacks){
753+                       signedCall('playlist.create', params, session, callbacks, 'POST');
754+               },
755+
756+               fetch : function(params, callbacks){
757+                       call('playlist.fetch', params, callbacks);
758+               }
759+       };
760+
761+       /* Radio methods. */
762+       this.radio = {
763+               getPlaylist : function(params, session, callbacks){
764+                       signedCall('radio.getPlaylist', params, session, callbacks);
765+               },
766+
767+               tune : function(params, session, callbacks){
768+                       signedCall('radio.tune', params, session, callbacks);
769+               }
770+       };
771+
772+       /* Tag methods. */
773+       this.tag = {
774+               getSimilar : function(params, callbacks){
775+                       call('tag.getSimilar', params, callbacks);
776+               },
777+
778+               getTopAlbums : function(params, callbacks){
779+                       call('tag.getTopAlbums', params, callbacks);
780+               },
781+
782+               getTopArtists : function(params, callbacks){
783+                       call('tag.getTopArtists', params, callbacks);
784+               },
785+
786+               getTopTags : function(callbacks){
787+                       call('tag.getTopTags', null, callbacks);
788+               },
789+
790+               getTopTracks : function(params, callbacks){
791+                       call('tag.getTopTracks', params, callbacks);
792+               },
793+
794+               getWeeklyArtistChart : function(params, callbacks){
795+                       call('tag.getWeeklyArtistChart', params, callbacks);
796+               },
797+
798+               getWeeklyChartList : function(params, callbacks){
799+                       call('tag.getWeeklyChartList', params, callbacks);
800+               },
801+
802+               search : function(params, callbacks){
803+                       call('tag.search', params, callbacks);
804+               }
805+       };
806+
807+       /* Tasteometer method. */
808+       this.tasteometer = {
809+               compare : function(params, callbacks){
810+                       call('tasteometer.compare', params, callbacks);
811+               }
812+       };
813+
814+       /* Track methods. */
815+       this.track = {
816+               addTags : function(params, session, callbacks){
817+                       signedCall('track.addTags', params, session, callbacks, 'POST');
818+               },
819+
820+               ban : function(params, session, callbacks){
821+                       signedCall('track.ban', params, session, callbacks, 'POST');
822+               },
823+
824+               getBuylinks : function(params, callbacks){
825+                       call('track.getBuylinks', params, callbacks);
826+               },
827+
828+               getInfo : function(params, callbacks){
829+                       call('track.getInfo', params, callbacks);
830+               },
831+
832+               getSimilar : function(params, callbacks){
833+                       call('track.getSimilar', params, callbacks);
834+               },
835+
836+               getTags : function(params, session, callbacks){
837+                       signedCall('track.getTags', params, session, callbacks);
838+               },
839+
840+               getTopFans : function(params, callbacks){
841+                       call('track.getTopFans', params, callbacks);
842+               },
843+
844+               getTopTags : function(params, callbacks){
845+                       call('track.getTopTags', params, callbacks);
846+               },
847+
848+               love : function(params, session, callbacks){
849+                       signedCall('track.love', params, session, callbacks, 'POST');
850+               },
851+
852+               removeTag : function(params, session, callbacks){
853+                       signedCall('track.removeTag', params, session, callbacks, 'POST');
854+               },
855+
856+               search : function(params, callbacks){
857+                       call('track.search', params, callbacks);
858+               },
859+
860+               share : function(params, session, callbacks){
861+                       /* Build comma separated recipients string. */
862+                       if(typeof(params.recipient) == 'object'){
863+                               params.recipient = params.recipient.join(',');
864+                       }
865+
866+                       signedCall('track.share', params, session, callbacks, 'POST');
867+               }
868+       };
869+
870+       /* User methods. */
871+       this.user = {
872+               getArtistTracks : function(params, callbacks){
873+                       call('user.getArtistTracks', params, callbacks);
874+               },
875+
876+               getEvents : function(params, callbacks){
877+                       call('user.getEvents', params, callbacks);
878+               },
879+
880+               getFriends : function(params, callbacks){
881+                       call('user.getFriends', params, callbacks);
882+               },
883+
884+               getInfo : function(params, callbacks){
885+                       call('user.getInfo', params, callbacks);
886+               },
887+
888+               getLovedTracks : function(params, callbacks){
889+                       call('user.getLovedTracks', params, callbacks);
890+               },
891+
892+               getNeighbours : function(params, callbacks){
893+                       call('user.getNeighbours', params, callbacks);
894+               },
895+
896+               getPastEvents : function(params, callbacks){
897+                       call('user.getPastEvents', params, callbacks);
898+               },
899+
900+               getPlaylists : function(params, callbacks){
901+                       call('user.getPlaylists', params, callbacks);
902+               },
903+
904+               getRecentStations : function(params, session, callbacks){
905+                       signedCall('user.getRecentStations', params, session, callbacks);
906+               },
907+
908+               getRecentTracks : function(params, callbacks){
909+                       call('user.getRecentTracks', params, callbacks);
910+               },
911+
912+               getRecommendedArtists : function(params, session, callbacks){
913+                       signedCall('user.getRecommendedArtists', params, session, callbacks);
914+               },
915+
916+               getRecommendedEvents : function(params, session, callbacks){
917+                       signedCall('user.getRecommendedEvents', params, session, callbacks);
918+               },
919+
920+               getShouts : function(params, callbacks){
921+                       call('user.getShouts', params, callbacks);
922+               },
923+
924+               getTopAlbums : function(params, callbacks){
925+                       call('user.getTopAlbums', params, callbacks);
926+               },
927+
928+               getTopArtists : function(params, callbacks){
929+                       call('user.getTopArtists', params, callbacks);
930+               },
931+
932+               getTopTags : function(params, callbacks){
933+                       call('user.getTopTags', params, callbacks);
934+               },
935+
936+               getTopTracks : function(params, callbacks){
937+                       call('user.getTopTracks', params, callbacks);
938+               },
939+
940+               getWeeklyAlbumChart : function(params, callbacks){
941+                       call('user.getWeeklyAlbumChart', params, callbacks);
942+               },
943+
944+               getWeeklyArtistChart : function(params, callbacks){
945+                       call('user.getWeeklyArtistChart', params, callbacks);
946+               },
947+
948+               getWeeklyChartList : function(params, callbacks){
949+                       call('user.getWeeklyChartList', params, callbacks);
950+               },
951+
952+               getWeeklyTrackChart : function(params, callbacks){
953+                       call('user.getWeeklyTrackChart', params, callbacks);
954+               },
955+
956+               shout : function(params, session, callbacks){
957+                       signedCall('user.shout', params, session, callbacks, 'POST');
958+               }
959+       };
960+
961+       /* Venue methods. */
962+       this.venue = {
963+               getEvents : function(params, callbacks){
964+                       call('venue.getEvents', params, callbacks);
965+               },
966+
967+               getPastEvents : function(params, callbacks){
968+                       call('venue.getPastEvents', params, callbacks);
969+               },
970+
971+               search : function(params, callbacks){
972+                       call('venue.search', params, callbacks);
973+               }
974+       };
975+
976+       /* Private auth methods. */
977+       var auth = {
978+               getApiSignature : function(params){
979+                       var keys   = [];
980+                       var string = '';
981+
982+                       for(var key in params){
983+                               keys.push(key);
984+                       }
985+
986+                       keys.sort();
987+
988+                       for(var index in keys){
989+                               var key = keys[index];
990+
991+                               string += key + params[key];
992+                       }
993+
994+                       string += apiSecret;
995+
996+                       /* Needs lastfm.api.md5.js. */
997+                       return md5(string);
998+               }
999+       };
1000+}
1001addfile ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/lastfm.api.md5.js
1002hunk ./contrib/musicplayer/src/libs/vendor/javascript-last.fm-api/lastfm.api.md5.js 1
1003+/*
1004+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
1005+ * Digest Algorithm, as defined in RFC 1321.
1006+ * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
1007+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
1008+ * Distributed under the BSD License
1009+ * See http://pajhome.org.uk/crypt/md5 for more info.
1010+ */
1011+
1012+/*
1013+ * Configurable variables. You may need to tweak these to be compatible with
1014+ * the server-side, but the defaults work in most cases.
1015+ */
1016+var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
1017+var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
1018+var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */
1019+
1020+/*
1021+ * These are the functions you'll usually want to call
1022+ * They take string arguments and return either hex or base-64 encoded strings
1023+ */
1024+function md5(s){ return hex_md5(s); }
1025+function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
1026+function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
1027+function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
1028+function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
1029+function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
1030+function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }
1031+
1032+/*
1033+ * Perform a simple self-test to see if the VM is working
1034+ */
1035+function md5_vm_test()
1036+{
1037+  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
1038+}
1039+
1040+/*
1041+ * Calculate the MD5 of an array of little-endian words, and a bit length
1042+ */
1043+function core_md5(x, len)
1044+{
1045+  /* append padding */
1046+  x[len >> 5] |= 0x80 << ((len) % 32);
1047+  x[(((len + 64) >>> 9) << 4) + 14] = len;
1048+
1049+  var a =  1732584193;
1050+  var b = -271733879;
1051+  var c = -1732584194;
1052+  var d =  271733878;
1053+
1054+  for(var i = 0; i < x.length; i += 16)
1055+  {
1056+    var olda = a;
1057+    var oldb = b;
1058+    var oldc = c;
1059+    var oldd = d;
1060+
1061+    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
1062+    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
1063+    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
1064+    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
1065+    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
1066+    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
1067+    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
1068+    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
1069+    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
1070+    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
1071+    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
1072+    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
1073+    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
1074+    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
1075+    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
1076+    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
1077+
1078+    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
1079+    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
1080+    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
1081+    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
1082+    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
1083+    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
1084+    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
1085+    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
1086+    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
1087+    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
1088+    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
1089+    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
1090+    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
1091+    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
1092+    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
1093+    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
1094+
1095+    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
1096+    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
1097+    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
1098+    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
1099+    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
1100+    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
1101+    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
1102+    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
1103+    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
1104+    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
1105+    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
1106+    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
1107+    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
1108+    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
1109+    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
1110+    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
1111+
1112+    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
1113+    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
1114+    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
1115+    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
1116+    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
1117+    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
1118+    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
1119+    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
1120+    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
1121+    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
1122+    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
1123+    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
1124+    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
1125+    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
1126+    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
1127+    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
1128+
1129+    a = safe_add(a, olda);
1130+    b = safe_add(b, oldb);
1131+    c = safe_add(c, oldc);
1132+    d = safe_add(d, oldd);
1133+  }
1134+  return Array(a, b, c, d);
1135+
1136+}
1137+
1138+/*
1139+ * These functions implement the four basic operations the algorithm uses.
1140+ */
1141+function md5_cmn(q, a, b, x, s, t)
1142+{
1143+  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
1144+}
1145+function md5_ff(a, b, c, d, x, s, t)
1146+{
1147+  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
1148+}
1149+function md5_gg(a, b, c, d, x, s, t)
1150+{
1151+  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
1152+}
1153+function md5_hh(a, b, c, d, x, s, t)
1154+{
1155+  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
1156+}
1157+function md5_ii(a, b, c, d, x, s, t)
1158+{
1159+  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
1160+}
1161+
1162+/*
1163+ * Calculate the HMAC-MD5, of a key and some data
1164+ */
1165+function core_hmac_md5(key, data)
1166+{
1167+  var bkey = str2binl(key);
1168+  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);
1169+
1170+  var ipad = Array(16), opad = Array(16);
1171+  for(var i = 0; i < 16; i++)
1172+  {
1173+    ipad[i] = bkey[i] ^ 0x36363636;
1174+    opad[i] = bkey[i] ^ 0x5C5C5C5C;
1175+  }
1176+
1177+  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
1178+  return core_md5(opad.concat(hash), 512 + 128);
1179+}
1180+
1181+/*
1182+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
1183+ * to work around bugs in some JS interpreters.
1184+ */
1185+function safe_add(x, y)
1186+{
1187+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
1188+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
1189+  return (msw << 16) | (lsw & 0xFFFF);
1190+}
1191+
1192+/*
1193+ * Bitwise rotate a 32-bit number to the left.
1194+ */
1195+function bit_rol(num, cnt)
1196+{
1197+  return (num << cnt) | (num >>> (32 - cnt));
1198+}
1199+
1200+/*
1201+ * Convert a string to an array of little-endian words
1202+ * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
1203+ */
1204+function str2binl(str)
1205+{
1206+  var bin = Array();
1207+  var mask = (1 << chrsz) - 1;
1208+  for(var i = 0; i < str.length * chrsz; i += chrsz)
1209+    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
1210+  return bin;
1211+}
1212+
1213+/*
1214+ * Convert an array of little-endian words to a string
1215+ */
1216+function binl2str(bin)
1217+{
1218+  var str = "";
1219+  var mask = (1 << chrsz) - 1;
1220+  for(var i = 0; i < bin.length * 32; i += chrsz)
1221+    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
1222+  return str;
1223+}
1224+
1225+/*
1226+ * Convert an array of little-endian words to a hex string.
1227+ */
1228+function binl2hex(binarray)
1229+{
1230+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
1231+  var str = "";
1232+  for(var i = 0; i < binarray.length * 4; i++)
1233+  {
1234+    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
1235+           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
1236+  }
1237+  return str;
1238+}
1239+
1240+/*
1241+ * Convert an array of little-endian words to a base-64 string
1242+ */
1243+function binl2b64(binarray)
1244+{
1245+  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1246+  var str = "";
1247+  for(var i = 0; i < binarray.length * 4; i += 3)
1248+  {
1249+    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
1250+                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
1251+                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
1252+    for(var j = 0; j < 4; j++)
1253+    {
1254+      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
1255+      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
1256+    }
1257+  }
1258+  return str;
1259+}
1260hunk ./contrib/musicplayer/src/libs/vendor/mootools-1.2.4-core-ui.js 3134
1261 ...
1262 */
1263 
1264-var JSON = new Hash(this.JSON && {
1265-       stringify: JSON.stringify,
1266-       parse: JSON.parse
1267-}).extend({
1268+// NOTE: This was modified in order to fix issue with browser which
1269+// have native implementations of JSON parser/serialiser
1270+
1271+if(typeof JSON === "undefined") {
1272+  var JSON = {};
1273
1274+  Native.implement([Hash, Array, String, Number], {
1275+         toJSON: function(){
1276+                 return JSON.encode(this);
1277+         }
1278+  });
1279+}
1280+
1281+$extend(JSON, {
1282       
1283        $specialChars: {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'},
1284 
1285hunk ./contrib/musicplayer/src/libs/vendor/mootools-1.2.4-core-ui.js 3182
1286 
1287 });
1288 
1289-Native.implement([Hash, Array, String, Number], {
1290-
1291-       toJSON: function(){
1292-               return JSON.encode(this);
1293-       }
1294-
1295-});
1296-
1297 
1298 /*
1299 ---
1300adddir ./contrib/musicplayer/src/libs/vendor/roar
1301addfile ./contrib/musicplayer/src/libs/vendor/roar/Roar.js
1302hunk ./contrib/musicplayer/src/libs/vendor/roar/Roar.js 1
1303-
1304+/**
1305+ * Roar - Notifications
1306+ *
1307+ * Inspired by Growl
1308+ *
1309+ * @version            1.0.1
1310+ *
1311+ * @license            MIT-style license
1312+ * @author             Harald Kirschner <mail [at] digitarald.de>
1313+ * @copyright  Author
1314+ */
1315+
1316+var Roar = new Class({
1317+
1318+       Implements: [Options, Events, Chain],
1319+
1320+       options: {
1321+               duration: 3000,
1322+               position: 'upperLeft',
1323+               container: null,
1324+               bodyFx: null,
1325+               itemFx: null,
1326+               margin: {x: 10, y: 10},
1327+               offset: 10,
1328+               className: 'roar',
1329+               onShow: $empty,
1330+               onHide: $empty,
1331+               onRender: $empty
1332+       },
1333+
1334+       initialize: function(options) {
1335+               this.setOptions(options);
1336+               this.items = [];
1337+               this.container = $(this.options.container) || document;
1338+       },
1339+
1340+       alert: function(title, message, options) {
1341+               var params = Array.link(arguments, {title: String.type, message: String.type, options: Object.type});
1342+               var items = [new Element('h3', {'html': $pick(params.title, '')})];
1343+               if (params.message) items.push(new Element('p', {'html': params.message}));
1344+               return this.inject(items, params.options);
1345+       },
1346+
1347+       inject: function(elements, options) {
1348+               if (!this.body) this.render();
1349+               options = options || {};
1350+
1351+               var offset = [-this.options.offset, 0];
1352+               var last = this.items.getLast();
1353+               if (last) {
1354+                       offset[0] = last.retrieve('roar:offset');
1355+                       offset[1] = offset[0] + last.offsetHeight + this.options.offset;
1356+               }
1357+               var to = {'opacity': 1};
1358+               to[this.align.y] = offset;
1359+
1360+               var item = new Element('div', {
1361+                       'class': this.options.className,
1362+                       'opacity': 0
1363+               }).adopt(
1364+                       new Element('div', {
1365+                               'class': 'roar-bg',
1366+                               'opacity': 0.7
1367+                       }),
1368+                       elements
1369+               );
1370+
1371+               item.setStyle(this.align.x, 0).store('roar:offset', offset[1]).set('morph', $merge({
1372+                       unit: 'px',
1373+                       link: 'cancel',
1374+                       onStart: Chain.prototype.clearChain,
1375+                       transition: Fx.Transitions.Back.easeOut
1376+               }, this.options.itemFx));
1377+
1378+               var remove = this.remove.create({
1379+                       bind: this,
1380+                       arguments: [item],
1381+                       delay: 10
1382+               });
1383+               this.items.push(item.addEvent('click', remove));
1384+
1385+               if (this.options.duration) {
1386+                       var over = false;
1387+                       var trigger = (function() {
1388+                               trigger = null;
1389+                               if (!over) remove();
1390+                       }).delay(this.options.duration);
1391+                       item.addEvents({
1392+                               mouseover: function() {
1393+                                       over = true;
1394+                               },
1395+                               mouseout: function() {
1396+                                       over = false;
1397+                                       if (!trigger) remove();
1398+                               }
1399+                       });
1400+               }
1401+               item.inject(this.body).morph(to);
1402+               return this.fireEvent('onShow', [item, this.items.length]);
1403+       },
1404+
1405+       remove: function(item) {
1406+               var index = this.items.indexOf(item);
1407+               if (index == -1) return this;
1408+               this.items.splice(index, 1);
1409+               item.removeEvents();
1410+               var to = {opacity: 0};
1411+               to[this.align.y] = item.getStyle(this.align.y).toInt() - item.offsetHeight - this.options.offset;
1412+               item.morph(to).get('morph').chain(item.destroy.bind(item));
1413+               return this.fireEvent('onHide', [item, this.items.length]).callChain(item);
1414+       },
1415+
1416+       empty: function() {
1417+               while (this.items.length) this.remove(this.items[0]);
1418+               return this;
1419+       },
1420+
1421+       render: function() {
1422+               this.position = this.options.position;
1423+               if ($type(this.position) == 'string') {
1424+                       var position = {x: 'center', y: 'center'};
1425+                       this.align = {x: 'left', y: 'top'};
1426+                       if ((/left|west/i).test(this.position)) position.x = 'left';
1427+                       else if ((/right|east/i).test(this.position)) this.align.x = position.x = 'right';
1428+                       if ((/upper|top|north/i).test(this.position)) position.y = 'top';
1429+                       else if ((/bottom|lower|south/i).test(this.position)) this.align.y = position.y = 'bottom';
1430+                       this.position = position;
1431+               }
1432+               this.body = new Element('div', {'class': 'roar-body'}).inject(document.body);
1433+               if (Browser.Engine.trident4) this.body.addClass('roar-body-ugly');
1434+               this.moveTo = this.body.setStyles.bind(this.body);
1435+               this.reposition();
1436+               if (this.options.bodyFx) {
1437+                       var morph = new Fx.Morph(this.body, $merge({
1438+                               unit: 'px',
1439+                               chain: 'cancel',
1440+                               transition: Fx.Transitions.Circ.easeOut
1441+                       }, this.options.bodyFx));
1442+                       this.moveTo = morph.start.bind(morph);
1443+               }
1444+               var repos = this.reposition.bind(this);
1445+               window.addEvents({
1446+                       scroll: repos,
1447+                       resize: repos
1448+               });
1449+               this.fireEvent('onRender', this.body);
1450+       },
1451+
1452+       reposition: function() {
1453+               var max = document.getCoordinates(), scroll = document.getScroll(), margin = this.options.margin;
1454+               max.left += scroll.x;
1455+               max.right += scroll.x;
1456+               max.top += scroll.y;
1457+               max.bottom += scroll.y;
1458+               var rel = ($type(this.container) == 'element') ? this.container.getCoordinates() : max;
1459+               this.moveTo({
1460+                       left: (this.position.x == 'right')
1461+                               ? (Math.min(rel.right, max.right) - margin.x)
1462+                               : (Math.max(rel.left, max.left) + margin.x),
1463+                       top: (this.position.y == 'bottom')
1464+                               ? (Math.min(rel.bottom, max.bottom) - margin.y)
1465+                               : (Math.max(rel.top, max.top) + margin.y)
1466+               });
1467+       }
1468+
1469+});
1470}
1471
1472Context:
1473
1474[added-songcontext-and-services
1475josip.lisec@gmail.com**20100731231316
1476 Ignore-this: e8cd467b7d5681d1b9fbf9decb8b1f4b
1477 * Added da.controller.SongContext with default contexts:
1478   * 'Artist' - shows basic information about the artist
1479     the currently playing song,
1480   * 'Recommendations' - shows similar artists and songs,
1481   * 'Music Videos' - presents search results from YouTube
1482     of currently playing song.
1483 * Added da.service.* APIs which are used by contexts above
1484   * da.service.lastFm - interface to the Last.fm services
1485   * da.service.artistInfo
1486   * da.service.recommendations
1487   * da.service.musicVideo
1488 * UTF-8 related fixes to ID3v2 parser
1489] 
1490[updated-docs
1491josip.lisec@gmail.com**20100731231211
1492 Ignore-this: cbfb98d6ebeed2f2826250eb43d912ff
1493 * Added NOTES file with instructions for running the tests.
1494 * Fixed typos in INSTALL
1495] 
1496[add-player-controls
1497josiplisec@gmail.com**20100728131919
1498 Ignore-this: 56f8d094357df285a679a04bf536c582
1499 * Added player controls (previos/play/next) with tests.
1500 * Made other, smaller, improvements to da.ui.NavigationColumn (smarter re-rendering)
1501 * Limited both scanner and indexer workers to allow only one request to Tahoe-LAFS,
1502   thus making network I/O almoast synchronous, mainly due to the fact that
1503   Tahoe never completes requests under high load.
1504 
1505] 
1506[add-controller-tests
1507josip.lisec@gmail.com**20100725094652
1508 Ignore-this: 506444f1ed082b7fd7caca82c1a2129f
1509 Added tests for:
1510   * da.ui.Dialog
1511   * da.controller.CollectionScanner
1512   * da.controller.Settings
1513] 
1514[player-interface
1515josip.lisec@gmail.com**20100719121357
1516 Ignore-this: b7287f386bcbfeb6e59b8686da0ec65c
1517  * Fixed bugs in (Segmented)ProgressBar
1518  * Added basic player controls
1519  * Added album cover fetcing via Last.fm
1520  * Reorganised the way external libraries are being imported
1521  * Fixed tests
1522] 
1523[add-progress-bars
1524josip.lisec@gmail.com**20100713181106
1525 Ignore-this: b96a74ab38924967a55383f75f888439
1526 Added da.ui.ProgressBar and da.ui.SegmentedProgressBar classes,
1527 the latter one will be used mainly for visualizing progress of
1528 currently playing song and load percentage.
1529] 
1530[add-navigation-and-settings-tests
1531josip.lisec@gmail.com**20100712142621
1532 Ignore-this: 559424eabbd88c496d69d076f2fcd5de
1533 Added tests for da.controller.Navigation, da.controller.Settings and
1534 da.controller.CollectionScanner.
1535] 
1536[add-music-players-full-deps
1537josip.lisec@gmail.com**20100710190714
1538 Ignore-this: 59d7b3890a1f9349bc8ac511af05b2b8
1539] 
1540[add-music-players-core-deps
1541josip.lisec@gmail.com**20100710185908
1542 Ignore-this: 58f5546ff75501f77d6b346ae99eebec
1543] 
1544[add-music-player
1545josip.lisec@gmail.com**20100710185704
1546 Ignore-this: c29dc0709640abd2e33cfd119b2681f
1547] 
1548[misc/build_helpers/run-with-pythonpath.py: fix stale comment, and remove 'trial' example that is not the right way to run trial.
1549david-sarah@jacaranda.org**20100726225729
1550 Ignore-this: a61f55557ad69a1633bfb2b8172cce97
1551] 
1552[docs/specifications/dirnodes.txt: 'mesh'->'grid'.
1553david-sarah@jacaranda.org**20100723061616
1554 Ignore-this: 887bcf921ef00afba8e05e9239035bca
1555] 
1556[docs/specifications/dirnodes.txt: bring layer terminology up-to-date with architecture.txt, and a few other updates (e.g. note that the MAC is no longer verified, and that URIs can be unknown). Also 'Tahoe'->'Tahoe-LAFS'.
1557david-sarah@jacaranda.org**20100723054703
1558 Ignore-this: f3b98183e7d0a0f391225b8b93ac6c37
1559] 
1560[docs: use current cap to Zooko's wiki page in example text
1561zooko@zooko.com**20100721010543
1562 Ignore-this: 4f36f36758f9fdbaf9eb73eac23b6652
1563 fixes #1134
1564] 
1565[__init__.py: silence DeprecationWarning about BaseException.message globally. fixes #1129
1566david-sarah@jacaranda.org**20100720011939
1567 Ignore-this: 38808986ba79cb2786b010504a22f89
1568] 
1569[test_runner: test that 'tahoe --version' outputs no noise (e.g. DeprecationWarnings).
1570david-sarah@jacaranda.org**20100720011345
1571 Ignore-this: dd358b7b2e5d57282cbe133e8069702e
1572] 
1573[TAG allmydata-tahoe-1.7.1
1574zooko@zooko.com**20100719131352
1575 Ignore-this: 6942056548433dc653a746703819ad8c
1576] 
1577Patch bundle hash:
1578493001be71babd5ed7d4fedc83b59a3734253d64