Ticket #1023: add-music-player.dpatch

File add-music-player.dpatch, 230.0 KB (added by josipl, at 2010-07-10T19:23:33Z)

Only the music player's code.

Line 
11 patch for repository /home/josip/bin/tahoe-tmp:
2
3Sat Jul 10 20:57:04 CEST 2010  josip.lisec@gmail.com
4  * add-music-player
5
6New patches:
7
8[add-music-player
9josip.lisec@gmail.com**20100710185704
10 Ignore-this: c29dc0709640abd2e33cfd119b2681f
11] {
12adddir ./contrib/musicplayer
13addfile ./contrib/musicplayer/INSTALL
14hunk ./contrib/musicplayer/INSTALL 1
15+=== Installing Music Player for Tahoe (codename 'Daaw') ===
16+
17+== Maths and Systems Theory quiz ==
18+If you already have a 'build' directory, feel free to skip this step.
19+
20+To build player's code you'll have to do a not-so-simple
21+operation of computing file dependencies, compressing variable names in JavaScript
22+code and stitching them all into one file.
23+
24+I strongly hope that you took advanced Maths, Systems Theory
25+and computing related courses.
26+
27+Just in case you haven't, you can type in next line into your shell:
28+  $ python manage.py build
29+  running build
30+  Calculating dependencies...
31+  Compressing <something>...
32+  ...
33+  Done!
34+
35+Bravo, you're done! (just make sure you have a 'build' directory)
36+
37+(And if you're one of those who prefer to do it by-hand (and keyboard),
38+this file isn't a place for you.)
39+
40+== Battle for Configuration File ==
41+Player's configuration file is a real beast on its own,
42+and in order to edit it we must prepare ourselves really good,
43+otherwise, we're doomed (actually, only you are )!
44+
45+Read next few steps carefully, the beast is just around the corner!
46+
47+1. Create two dirnodes on your Tahoe-LAFS server, one will be used for storing
48+   all your music files and the other one for syncing settings between multiple
49+   computers.
50
51+   Just in case you've forgotten how to create Tahoe dirnodes, run this from your
52+   shell:
53+   $ tahoe mkdir music
54+   <top secret no.1>
55+   $ tahoe mkdir settings
56+   <top secret no.2>
57
58+   (make sure Tahoe-LAFS is running on your computer before issuing those commands)
59+
60+2. Take a big breath, as we're about to open example configuration file!
61+
62+3. Yep, now open the 'config.example.json' file in your favourite text editor.
63+   Now quickly, we have to replace her evil genes with a good ones,
64+   find following line in her DNA sequence:
65
66+      "music_cap": "<bad gene no.1>",
67+      "settings_cap": "<bad gene no.2>"
68
69+  and quickly replace <bad gene no.1> with <top secret no.1> as well as <bad gene no.2>
70+  with <top secret no.2>.
71
72+  If you're still here, congrats!
73
74+  (The truth about <top secret>s is that your Tahoe-LAFS installation actually
75+  knows how to re-sequence DNA of living beings, and we don't want others to
76+  find out about that and use it in evil purposes, don't we?)
77
78+  Now save the new genes under name of 'config.json'.
79+
80+== The Critical Step ==
81+After we've conquered the beast of configuration file we're ready to
82+upload the player to the Tahoe!
83+
84+To do that, just copy the 'build' directory to 'public_html' directory of your
85+Tahoe storage node (usually ~/.tahoe).
86+Note that 'public_html' directory is probably missing,
87+so you'll have to create it on your own.
88+
89+  $ mkdir -p ~/.tahoe/public_html/musicplayer
90+  $ cp -r build/ ~/.tahoe/public_html/musicplayer
91+
92+WARNING: If you don't perform next step exactly as
93+you're instructed, the whole process could fail and you'll
94+have to start all over!
95+
96+Now, stand up, and with evident excitement on your face,
97+say the following phrase:
98+  "Yay! It's working!"
99+
100+== Fin ==
101+You can now upload your music to the <top secret no.1> dirnode and
102+launch music player by typing this URL into your web browser:
103+  http://localhost:3456/static/musicplayer
104+
105+If it appears that something isn't working, it probably means
106+that you haven't read 'The Critical Step' carefully enough.
107+
108+We hope you're going to enjoy your music even more with Music Player for Tahoe-LAFS!
109addfile ./contrib/musicplayer/manage.py
110hunk ./contrib/musicplayer/manage.py 1
111+#!/usr/bin/env python
112+# -*- coding: utf-8 -*-
113+
114+import os, shutil, sys, subprocess, re
115+from time import sleep
116+from setuptools import setup
117+from setuptools import Command
118+
119+CLOSURE_COMPILER_PATH = 'tools/closure-compiler-20100514/compiler.jar'
120+
121+class JSDepsBuilder:
122+  """
123+  Looks for
124+    //#require "file.js"
125+  and
126+    //#require <file.js>
127+  lines in JavaScript files and creates a file with all the required files.
128+  """
129+  requires_re = re.compile('^//#require ["|\<](.+)["|\>]$', re.M)
130
131+  def __init__(self, root_directory):
132+    self.files        = {}
133+    self.included     = []
134+    self.root         = root_directory   
135+   
136+    self.scan()
137
138+  def scan(self):
139+    for (dirname, dirs, files) in os.walk(self.root):
140+      for filename in files:
141+        if filename.endswith('.js'):
142+          self.detect_requires(os.path.join(dirname, filename))
143
144+  def detect_requires(self, path):
145+    reqs = []
146+    script_file = open(path, 'r')
147+    script = script_file.read()
148+    script_file.close()
149+   
150+    reqs = re.findall(JSDepsBuilder.requires_re, script)
151+    for i in range(len(reqs)):
152+      req_path = os.path.join(self.root, reqs[i])
153+      reqs[i] = req_path
154+      if not os.path.isdir(req_path) and not req_path.endswith('.js'):       
155+        reqs[i] += '.js'
156+   
157+    #if len(reqs):
158+    #  print '%s depends on:' % os.path.basename(path)
159+    #  print '\t', '\n\t'.join(reqs)
160+
161+    self.files[path] = reqs
162+
163+  def parse(self, path):
164+    if path in self.included:
165+      return ''
166+    if not path.endswith('.js'):
167+      # TODO: If path points to a directory, require all the files within that directory.
168+      return ''
169+   
170+    def insert_code(match):
171+      req_path = os.path.join(self.root, match.group(1))
172+      if not req_path in self.included:
173+        if not os.path.isfile(req_path):
174+          raise Exception('%s requires non existing file: %s' % (path, req_path))
175+         
176+        return self.parse(req_path)
177+     
178+    script_file = open(path, 'r')
179+    script = script_file.read()
180+    script_file.close()
181+    script = re.sub(JSDepsBuilder.requires_re, insert_code, script)
182+    self.included.append(path)
183+
184+    return script
185
186+  def write_to_file(self, filename, root_file = 'Application.js'):
187+    output = open(filename, 'w+')
188+    self.included = []
189+    output.write(self.parse(os.path.join(self.root, root_file)))
190+    output.close()
191+   
192+  def print_script_tags(self, root_file = 'Application.js'):
193+    self.included = []
194+    self.parse(os.path.join(self.root, root_file))
195+   
196+    for filename in self.included:
197+      print '<script src="%s" type="text/javascript" charset="utf-8"></script>' % filename
198+   
199+class Build(Command):
200+  description = 'builds whole application into build directory'
201+  user_options = [
202+    ('compilation-level=', 'c', 'compilation level for Google\'s Closure compiler.'),
203+  ]
204
205+  def initialize_options(self):
206+    self.compilation_level = 'SIMPLE_OPTIMIZATIONS'
207+   
208+  def finalize_options(self):
209+    compilation_levels = [
210+      'SIMPLE_OPTIMIZATIONS', 'WHITESPACE_ONLY', 'ADVANCED_OPTIMIZATIONS', 'NONE'
211+    ]
212+   
213+    self.compilation_level = self.compilation_level.upper()
214+    if not self.compilation_level in compilation_levels:
215+      self.compilation_level = compilation_levels[0]
216
217+  def run(self):
218+    if os.path.isdir('build'):
219+      shutil.rmtree('build')
220+   
221+    shutil.copytree('src/resources', 'build/resources')
222+    shutil.copytree('src/plugins', 'build/plugins')
223+    shutil.copy('src/config.example.json', 'build/')
224+    shutil.copy('src/index.html', 'build/')
225+   
226+    shutil.copytree('src/libs/vendor/soundmanager/swf', 'build/resources/flash')
227+    shutil.copy('src/libs/vendor/persist-js/persist.swf', 'build/resources/flash')
228+   
229+    os.makedirs('build/js/libs')
230+    os.makedirs('build/js/workers')
231+    shutil.copy('src/libs/vendor/browser-couch/js/worker-map-reducer.js', 'build/js/workers/map-reducer.js')
232+   
233+    print 'Calculating dependencies...'
234+    appjs = JSDepsBuilder('src/')
235+    appjs.write_to_file('build/app.js')
236+    self._compress('build/js/app.js', ['build/app.js'])
237+    os.remove('build/app.js')
238+   
239+    # Libraries used by web workers
240+    self._compile_js('src/libs', 'build/js/workers/env.js', files = [
241+      'vendor/mootools-1.2.4-core-server.js',
242+      'vendor/mootools-1.2.4-request.js',
243+     
244+      'util/util.js',
245+      'util/BinaryFile.js',
246+      'util/ID3.js',
247+      'util/ID3v2.js',
248+      'util/ID3v1.js',
249+      'TahoeObject.js'
250+    ])
251+   
252+    # Compressing the workers themselves
253+    self._compile_js('src/workers', 'build/js/workers/', join = False)
254+   
255+    print 'Done!'
256
257+  def _compile_js(self, source, output_file, files = None, join = True):
258+    js_files = files
259+    if not js_files:
260+      js_files = []
261+      for filename in os.listdir(source):
262+        if filename.endswith('.js'):
263+          js_files.append(os.path.join(source, filename))
264+         
265+      js_files.sort()
266+    else:
267+      js_files = [os.path.join(source, path) for path in files]
268+   
269+    if join:
270+      self._compress(output_file, js_files)
271+    else:
272+      for js_file in js_files:
273+        self._compress(output_file + os.path.basename(js_file), [js_file])
274
275+  def _compress(self, output_file, files):
276+    print 'Compressing %s...' % output_file
277+   
278+    if self.compilation_level == 'NONE':
279+      output_file = open(output_file, 'a')
280+      for filename in files:
281+        f = open(filename)
282+        output_file.write(f.read())
283+        output_file.write('\n')
284+        f.close()
285+
286+      output_file.close()
287+    else:
288+      args = [
289+        'java',
290+        '-jar',                 CLOSURE_COMPILER_PATH,
291+        '--warning_level',      'QUIET',
292+        '--compilation_level',  self.compilation_level,
293+        '--js_output_file',     output_file]
294+     
295+      for filename in files:
296+        args.append('--js')
297+        args.append(filename)
298+
299+      subprocess.call(args)
300+
301+class Watch(Build):
302+  description = 'watches src directory for changes and runs build command when they occur'
303
304+  def run(self):
305+    self.dirs = {}
306+   
307+    while True:
308+      if self._watch_dir():
309+        print 'Watching for changes...'
310+      sleep(5)
311
312+  def _watch_dir(self):
313+    should_build = False
314+    for (root, dirs, files) in os.walk('src'):
315+      for file in files:
316+        path = root + '/' + file
317+        mtime = os.stat(path).st_mtime
318+       
319+        if not path in self.dirs:
320+          self.dirs[path] = 0
321+       
322+        if self.dirs[path] != mtime:
323+          should_build = True
324+          self.dirs[path] = mtime
325+          print '\t* ' + path
326+   
327+    if should_build:
328+      Build.run(self)
329+      return True
330+    else:
331+      return False
332+
333+
334+class Package(Build):
335+  description = 'builds application and creates a .tar.gz archive'
336+  user_options = []
337
338+  def initalize_options(self):
339+    pass
340+  def finalize_options(self):
341+    pass
342+
343+  def run(self):
344+    Build.run(self)
345+   
346+
347+class Install(Command):
348+  description = 'copies application to storage node\'s public_html and writes configuration files'
349+  user_options = []
350
351+  def initalize_options(self):
352+    pass
353+  def finalize_options(self):
354+    pass
355+  def run(self):
356+    pass
357+
358+class Docs(Command):
359+  description = 'generate documentation'
360+  user_options = []
361
362+  def initialize_options(self):
363+    pass
364+  def finalize_options(self):
365+    pass
366
367+  def run(self):
368+    if os.path.isdir('docs'):
369+      shutil.rmtree('docs')
370+   
371+    args = ['pdoc', '-o', 'docs']
372+   
373+    root_dirs = [
374+      'src/', 'src/libs', 'src/libs/ui', 'src/libs/db',
375+      'src/libs/util', 'src/controllers', 'src/doctemplates'
376+    ]
377+    for root_dir in root_dirs:
378+      for filename in os.listdir(root_dir):
379+        if filename.endswith('.js'):
380+          args.append(os.path.join(root_dir, filename))
381+   
382+    subprocess.call(args)
383+
384+setup(
385+  name = 'tahoe-music-player',
386+  cmdclass = {
387+    'build':    Build,
388+    'install':  Install,
389+    'watch':    Watch,
390+    'docs':     Docs
391+  }
392+)
393adddir ./contrib/musicplayer/src
394addfile ./contrib/musicplayer/src/Application.js
395hunk ./contrib/musicplayer/src/Application.js 1
396+//#require "libs/vendor/mootools-1.2.4-core-ui.js"
397+//#require "libs/vendor/mootools-1.2.4.4-more.js"
398+
399+/**
400+ * da
401+ *
402+ * The root namespace. Shorthand for '[Daaw](http://en.wikipedia.org/wiki/Lake_Tahoe#Native_people)'.
403+ *
404+ **/
405+if(typeof da === "undefined")
406+  this.da = {};
407+
408+//#require "libs/db/PersistStorage.js"
409+//#require "libs/db/DocumentTemplate.js"
410+//#require "libs/util/Goal.js"
411+
412+(function () {
413+var BrowserCouch    = da.db.BrowserCouch,
414+    PersistStorage  = da.db.PersistStorage,
415+    Goal            = da.util.Goal;
416+
417+/** section: Controllers
418+ *  class da.app
419+ *
420+ *  The main controller. All methods are public.
421+ **/
422+da.app = {
423+  /**
424+   *  da.app.caps -> Object
425+   *  Object with `music` and `settings` properties, ie. the contents of `config.json` file.
426+   **/
427+  caps: {},
428
429+  initialize: function () {
430+    this.startup = new Goal({
431+      checkpoints: ["domready", "settings_db", "caps", "data_db", "soundmanager"],
432+      onFinish: this.ready.bind(this)
433+    });
434+   
435+    BrowserCouch.get("settings", function (db) {
436+      da.db.SETTINGS = db;
437+      if(!db.getLength())
438+        this.loadInitialSettings();
439+      else {
440+        this.startup.checkpoint("settings_db");
441+        this.getCaps();
442+      }
443+    }.bind(this), new PersistStorage("tahoemp_settings"));
444+   
445+    BrowserCouch.get("data", function (db) {
446+      da.db.DEFAULT = db;
447+      this.startup.checkpoint("data_db");
448+    }.bind(this), new PersistStorage("tahoemp_data"));
449+  },
450
451+  loadInitialSettings: function () {
452+    new Request.JSON({
453+      url: "config.json",
454+      noCache: true,
455+     
456+      onSuccess: function (data) {
457+        da.db.SETTINGS.put([
458+          {id: "music_cap",     type: "Setting", group_id: "caps", value: data.music_cap},
459+          {id: "settings_cap",  type: "Setting", group_id: "caps", value: data.settings_cap}
460+        ], function () {
461+          this.startup.checkpoint("settings_db");
462+
463+          this.caps.music = data.music_cap,
464+          this.caps.settings = data.settings_cap;
465+
466+          this.startup.checkpoint("caps");
467+        }.bind(this));
468+      }.bind(this),
469+     
470+      onFailure: function () {
471+        alert("You're missing a config.json file! See docs on how to set it up.");
472+        var showSettings = function () {
473+          da.controller.Settings.showGroup("caps");
474+        };
475+       
476+        if(da.controller.Settings)
477+          showSettings();
478+        else
479+          da.app.addEvent("ready.controller.Settings", showSettings);
480+      }
481+    }).get()
482+  },
483
484+  getCaps: function () {
485+    // We can't use DocumentTemplate.Setting here as the class
486+    // is usually instantiated after the call to this function.
487+    da.db.SETTINGS.view({
488+      id: "caps",
489+     
490+      map: function (doc, emit) {
491+        if(doc && doc.type === "Setting" && doc.group_id === "caps")
492+          emit(doc.id, doc.value);
493+      },
494+     
495+      finished: function (result) {
496+        this.caps.music = result.getRow("music_cap");
497+        this.caps.settings = result.getRow("settings_cap");
498+        if(!this.caps.music.length || !this.caps.music.length)
499+          this.loadInitialSettings();
500+        else
501+          this.startup.checkpoint("caps");
502+      }.bind(this),
503+     
504+      updated: function (result) {
505+        if(result.findRow("music_cap") !== -1) {
506+          this.caps.music = result.getRow("music_cap");
507+          da.controller.CollectionScanner.scan(this.caps.music);
508+        }
509+        if(result.findRow("settings_cap") !== -1)
510+          this.caps.settings = result.getRow("settings_cap")
511+      }.bind(this)
512+    });
513+  },
514
515+  /**
516+   *  da.app.ready() -> undefined
517+   *  fires ready
518+   * 
519+   *  Called when all necessary components are initialized.
520+   **/
521+  ready: function () {
522+    $("loader").destroy();
523+    $("panes").setStyle("display", "block");
524+   
525+    if(da.db.DEFAULT.getLength() === 0)
526+      da.controller.CollectionScanner.scan();
527+
528+    this.fireEvent("ready");
529+  }
530+};
531+$extend(da.app, new Events());
532+
533+da.app.initialize();
534+
535+window.addEvent("domready", function () {
536+  da.app.startup.checkpoint("domready");
537+});
538+
539+})();
540+
541+//#require <doctemplates/doctemplates.js>
542+//#require <controllers/controllers.js>
543addfile ./contrib/musicplayer/src/config.example.json
544hunk ./contrib/musicplayer/src/config.example.json 1
545+{
546+  "music_cap":    "URI:DIR2:yz6mfqhmnog7jti65rblzwrdxe:7pyqgnikbn4iklmcst6n7hwgmkoim24dfxfm3y4374oi755yhyta",
547+  "settings_cap": "URI:DIR2:i564xjoawurnbrkaevyzdufqzi:mqnvdbqzia3euvorf2dwte6jdb6hnmwlxa4i7syw63kly4ubndda"
548+}
549adddir ./contrib/musicplayer/src/controllers
550addfile ./contrib/musicplayer/src/controllers/CollectionScanner.js
551hunk ./contrib/musicplayer/src/controllers/CollectionScanner.js 1
552+//#require "libs/util/Goal.js"
553+//#require "doctemplates/Song.js"
554+//#require "doctemplates/Artist.js"
555+//#require "doctemplates/Album.js"
556+
557+(function () {
558+var DocumentTemplate  = da.db.DocumentTemplate,
559+    Song              = DocumentTemplate.Song,
560+    Artist            = DocumentTemplate.Artist,
561+    Album             = DocumentTemplate.Album,
562+    Goal              = da.util.Goal;
563+
564+/** section: Controllers
565+ *  class CollectionScanner
566+ * 
567+ *  Controller which operates with [[Scanner]] and [[Indexer]] WebWorkers.
568+ * 
569+ *  #### Notes
570+ *  This is private class.
571+ *  Public interface is provided via [[da.controller.CollectionScanner]].
572+ **/
573+var CollectionScanner = new Class({
574+  /**
575+   *  new CollectionScanner()
576+   * 
577+   *  Starts a new scan using [[Application.caps.music]] as root directory.
578+   **/
579+  initialize: function (root) {
580+    this.indexer = new Worker("js/workers/indexer.js");
581+    this.indexer.onmessage = this.onIndexerMessage.bind(this);
582+   
583+    this.scanner = new Worker("js/workers/scanner.js");
584+    this.scanner.onmessage = this.onScannerMessage.bind(this);
585+   
586+    this.scanner.postMessage(root || da.app.caps.music);
587+   
588+    this.finished = false;
589+   
590+    this._goal = new Goal({
591+      checkpoints: ["scanner", "indexer"],
592+      onFinish: function () {
593+        this.finished = true;
594+        this.terminate()
595+      }.bind(this)
596+    })
597+  },
598
599+  /**
600+   *  CollectionScanner#finished -> true | false
601+   **/
602+  finished: false,
603
604+  /**
605+   *  CollectionScanner#terminate() -> undefined
606+   * 
607+   *  Instantly kills both workers.
608+   **/
609+  terminate: function () {
610+    this.indexer.terminate();
611+    this.scanner.terminate();
612+  },
613
614+  onScannerMessage: function (event) {
615+    var cap = event.data;
616+    if(cap === "**FINISHED**") {
617+      this._goal.checkpoint("scanner");
618+      return; // this.scanner.terminate();
619+    }
620+   
621+    if(da.db.DEFAULT.views.Song.view.findRow(cap) === -1)
622+      this.indexer.postMessage(cap);
623+  },
624
625+  onIndexerMessage: function (event) {
626+    if(event.data === "**FINISHED**") {
627+      this._goal.checkpoint("indexer");
628+      return; //this.indexer.terminate();
629+    }
630+
631+    // Lots of async stuff is going on, a short summary would look something like:
632+    // 1. find or create artist with given name and save its id
633+    //    to artist_id.
634+    // 2. look for an album with given artist_id (afterCheckpoint.artist)
635+    // 3. save the album data.
636+    // 4. look for song with given id and save the new data.
637+   
638+    var tags = event.data,
639+        album_id, artist_id,
640+        links = new Goal({
641+          checkpoints: ["artist", "album"],
642+          onFinish: function () {
643+            Song.findOrCreate({
644+              properties: {id: tags.id},
645+              onSuccess: function (song) {
646+                song.update({
647+                  title:      tags.title,
648+                  track:      tags.track,
649+                  year:       tags.year,
650+                  lyrics:     tags.lyrics,
651+                  genre:      tags.genere,
652+                  artist_id:  artist_id,
653+                  album_id:   album_id
654+                });
655+              }
656+            });
657+          },
658+         
659+          afterCheckpoint: {
660+            artist: function () {
661+              Album.findOrCreate({
662+                properties: {artist_id: artist_id, title: tags.album},
663+                onSuccess: function (album, wasCreated) {
664+                  album_id = album.id;
665+                  if(wasCreated)
666+                    album.save(function () { links.checkpoint("album"); })
667+                  else
668+                    links.checkpoint("album");
669+                }
670+              });
671+            }
672+          }
673+        });
674+   
675+    Artist.findOrCreate({
676+      properties: {title: tags.artist},
677+      onSuccess: function (artist, was_created) {
678+        artist_id = artist.id;
679+        if(was_created)
680+          artist.save(function () { links.checkpoint("artist"); });
681+        else
682+          links.checkpoint("artist");
683+      }
684+    });
685+  }
686+});
687+
688+var SCANNER;
689+/**
690+ * da.controller.CollectionScanner
691+ **/
692+da.controller.CollectionScanner = {
693+  /**
694+   *  da.controller.CollectionScanner.scan() -> undefined
695+   *  Starts scanning music directory
696+   *
697+   *  Part of public API.
698+   **/
699+  scan: function (cap) {
700+    if(!SCANNER || (SCANNER && SCANNER.finished))
701+      SCANNER = new CollectionScanner(cap);
702+  },
703
704+  /**
705+   *  da.controller.CollectionScanner.isScanning() -> true | false
706+   *
707+   *  Part of public API.
708+   **/
709+  isScanning: function () {
710+    return SCANNER && !SCANNER.finished;
711+  }
712+};
713+
714+da.app.fireEvent("ready.controller.CollectionScanner", [], 1);
715+})();
716addfile ./contrib/musicplayer/src/controllers/Navigation.js
717hunk ./contrib/musicplayer/src/controllers/Navigation.js 1
718+//#require "libs/ui/Menu.js"
719+(function () {
720+var Menu = da.ui.Menu;
721+   
722+/** section: Controllers
723+ *  class NavigationColumnContainer
724+ * 
725+ *  Class for managing column views.
726+ *
727+ *  #### Notes
728+ *  This class is private.
729+ *  Public interface is accessible via [[da.controller.Navigation]].
730+ **/
731+
732+var NavigationColumnContainer = new Class({
733+  /**
734+   *  new NavigationColumnContainer(options)
735+   *  - options.columnName (String): name of the column.
736+   *  - options.container (Element): container element.
737+   *  - options.header (Element): header element.
738+   *  - options.menu (UI.Menu): [[UI.Menu]] instance.
739+   * 
740+   *  Renders column and adds self to the [[da.controller.Navigation.activeColumns]].
741+   **/
742+   
743+  /**
744+    *  NavigationColumnContainer#column_name -> String
745+    *  Name of the column.
746+    **/
747
748+  /**
749+   *  NavigationColumnContainer#column -> NavigationColumn
750+   *  `column` here represents the list itself.
751+   **/
752
753+  /**
754+   *  NavigationColumnContainer#parent_column -> NavigationColumnContainer
755+   *  Usually column which created _this_ one. Visually, its the one to the left of _this_ one.
756+   **/
757+   
758+  /**
759+    *  NavigationColumnContainer#header -> Element
760+    *  Header element. It's an `a` tag with an `span` element.
761+    *  `a` tag has `column_header`, while `span` tag has `column_title` CSS class.
762+    **/
763
764+  /**
765+   *  NavigationColumnContainer#menu -> UI.Menu
766+   *  Container's [[UI.Menu]]. It can be also accesed with:
767+   * 
768+   *        this.header.retrieve("menu")
769+   **/
770+   
771+  /**
772+   *  NavigationColumnContainer#_el -> Element
773+   *  [[Element]] of the actual container. Has `column_container` CSS class.
774+   **/
775+  initialize: function (options) {
776+    this.column_name = options.columnName;
777+    this.parent_column = Navigation.activeColumns[Navigation.activeColumns.push(this) - 2];
778+   
779+    if(!(this._el = options.container))
780+      this.createContainer();
781+
782+    if(!(this.header = options.header))
783+      this.createHeader();
784+   
785+    this.column = new Navigation.columns[this.column_name]({
786+      filter: options.filter,
787+      parentElement: this._el
788+    });
789+    Navigation.adjustColumnSize(this.column);
790+
791+    if(!(this.menu = options.menu))
792+      this.createMenu();
793+   
794+    if(this.column.constructor.filters && this.column.constructor.filters.length)
795+      this.column.addEvent("click", this.listItemClick.bind(this));
796+   
797+    var first_item = this.column._el.getElement(".column_item");
798+    if(first_item)
799+      first_item.focus();
800+  },
801
802+  /**
803+   *  NavigationColumnContainer#createContainer() -> this
804+   * 
805+   *  Creates container element in `navigation_pane` [[Element]].
806+   **/
807+  createContainer: function () {
808+    $("navigation_pane").grab(this._el = new Element("div", {
809+      id: this.column_name + "_column_container",
810+      "class": "column_container no_selection"
811+    }));
812+   
813+    return this;
814+  },
815
816+  /**
817+   *  NavigationColumnContainer#createHeader() -> this
818+   * 
819+   *  Creates header element and attaches click event. Element is added to [[NavigationColumnContainer#toElement]].
820+   **/
821+  createHeader: function () {
822+    this.header = new Element("a", {
823+      "class": "column_header",
824+      href: "#"
825+    });
826+   
827+    this.header.addEvent("click", function (e) {
828+      var menu = this.retrieve("menu");
829+      if(menu)
830+        menu.show(e);
831+    });
832+   
833+    this._el.grab(this.header.grab(new Element("span", {
834+      html: this.column_name,
835+      "class": "column_title"
836+    })));
837+   
838+    return this;
839+  },
840
841+  /**
842+   *  NavigationColumnContainer#createMenu() -> this | false
843+   * 
844+   *  Creates menu for current column if it has filters.
845+   *  [[da.ui.Menu]] instance is stored to `header` element with `menu` key.
846+   **/
847+  createMenu: function () {
848+    var filters = this.column.constructor.filters,
849+        items = {};
850+   
851+    if(!filters || !filters.length)
852+      return false;
853+   
854+    items[this.column_name] = {html: this.column.constructor.title, "class": "checked", href: "#"};
855+    for(var n = 0, m = filters.length; n < m; n++)
856+      items[filters[n]] = {html: filters[n], href: "#"};
857+   
858+    this.menu = new Menu({
859+      items: items
860+    });
861+   
862+    this.menu._el.addClass("navigation_menu");
863+    this.header.store("menu", this.menu);
864+   
865+    this.menu.addEvent("show", function () {
866+      var header = this.header;
867+      header.addClass("active");
868+      header.retrieve("menu")._el.style.width = header.getWidth() + "px";
869+    }.bind(this));
870+   
871+    this.menu.addEvent("hide", function () {
872+      this.header.removeClass("active");
873+    }.bind(this));
874+   
875+    if(filters && filters.length)
876+      this.menu.addEvent("click", this.menuItemClick.bind(this.parent_column || this));
877+   
878+    return this;
879+  },
880
881+  /**
882+   *  NavigationColumnContainer#menuItemClick(filterName, event, element) -> undefined
883+   *  - filterName (String): id of the menu item.
884+   *  - event (Event): DOM event.
885+   *  - element (Element): clicked `Element`.
886+   * 
887+   *  Function called on menu click. If `filterName` is name of an actual filter then
888+   *  list in current column is replaced with a new one (provided by that filter).
889+   **/
890+  menuItemClick: function (filter_name, event, element) {
891+    if(!Navigation.columns[filter_name])
892+      return;
893+   
894+    var parent = this.filter_column._el,
895+        header = this.filter_column.header,
896+        menu   = this.filter_column.menu;
897+   
898+    // we need to keep the menu and header, since
899+    // all we need to do is to replace the list
900+    this.filter_column.menu = null;
901+    this.filter_column._el = null;
902+    this.filter_column.destroy();
903+   
904+    this.filter_column = new NavigationColumnContainer({
905+      columnName: filter_name,
906+      filter: this.filter_column.column.options.filter,
907+      container: parent,
908+      header: header,
909+      menu: menu
910+    });
911+       
912+    if(menu.last_clicked)
913+      menu.last_clicked.removeClass("checked");
914+    element.addClass("checked");
915+   
916+    header.getElement(".column_title").empty().appendText(filter_name);
917+  },
918
919+  /**
920+   *  NavigationColumnContainer#listItemClick(item) -> undefined
921+   *  - item (Object): clicked item.
922+   * 
923+   *  Creates a new column after this one with applied filter.
924+   **/
925+  listItemClick: function (item) {
926+    if(this.filter_column)
927+      this.filter_column.destroy();
928+   
929+    this.filter_column = new NavigationColumnContainer({
930+      columnName: this.column.constructor.filters[0],
931+      filter: this.column.createFilter(item)
932+    });
933+  },
934
935+  /**
936+   *  NavigationColumnContainer#destroy() -> this
937+   * 
938+   *  Destroys this column (including menu).
939+   *  Removes itself from [[da.controller.Navigation.activeColumns]].
940+   **/
941+  destroy: function () {
942+    if(this.filter_column) {
943+      this.filter_column.destroy();
944+      delete this.filter_column;
945+    }
946+    if(this.menu) {
947+      this.menu.destroy();
948+      delete this.menu;
949+    }
950+    this.column.destroy();
951+    delete this.column;
952+    if(this._el) {
953+      this._el.destroy();
954+      delete this._el;
955+    }
956+   
957+    var index = Navigation.activeColumns.indexOf(this);
958+    delete Navigation.activeColumns[index];
959+    Navigation.activeColumns = Navigation.activeColumns.clean();
960+   
961+    return this;
962+  },
963
964+  /**
965+   *  NavigationColumnContainer#toElement() -> Element
966+   **/
967+  toElement: function () {
968+    return this._el;
969+  }
970+});
971+
972+/** section: Controllers
973+ * da.controller.Navigation
974+ **/
975+var Navigation = {
976+  // This is not really a class, but PDoc refuses to generate docs otherwise.
977+  /**
978+   *  da.controller.Navigation.columns
979+   * 
980+   *  Contains all known columns.
981+   *
982+   *  #### Notes
983+   *  Use [[da.controller.Navigation.registerColumn]] to add new ones,
984+   *  *do not* add them manually.
985+   **/
986+  columns: {},
987
988+  /**
989+   *  da.controller.Navigation.activeColumns -> [NavigationColumnContainer, ...]
990+   * 
991+   *  Array of currently active columns.
992+   *  The first column is always [[da.controller.Navigation.columns.Root]].
993+   **/
994+  activeColumns: [],
995
996+  initialize: function () {
997+    var root_column = new NavigationColumnContainer({columnName: "Root"});
998+    root_column.menu.removeItem("Root");
999+    root_column.menu.addItems({
1000+      separator_1: Menu.separator,
1001+      search:     {html: "Search",    href:"#"},
1002+      settings:   {html: "Settings",  href:"#", events: {click: da.controller.Settings.show}},
1003+      help:       {html: "Help",      href:"#"}
1004+    });
1005+   
1006+    var artists_column = new NavigationColumnContainer({
1007+      columnName: "Artists",
1008+      menu: root_column.menu
1009+    });
1010+    artists_column.header.store("menu", root_column.menu);
1011+    root_column.filter_column = artists_column;
1012+    root_column.header = artists_column.header;
1013+   
1014+    this._header_height = root_column.header.getHeight();
1015+    window.addEvent("resize", function () {
1016+      var columns = Navigation.activeColumns,
1017+          n = columns.length,
1018+          windowHeight = window.getHeight();
1019+     
1020+      while(n--)
1021+        columns[n].column._el.setStyle("height", windowHeight - this._header_height);
1022+    }.bind(this));
1023+   
1024+    window.fireEvent("resize");
1025+  },
1026
1027+  /**
1028+   *  da.controller.Navigation.adjustColumnSize(column) -> undefined
1029+   *  - column (da.ui.NavigationColumn): column which needs size adjustment.
1030+   * 
1031+   *  Adjusts column's height to window.
1032+   **/
1033+  adjustColumnSize: function (column) {
1034+    column._el.setStyle("height", window.getHeight() - this._header_height);
1035+  },
1036
1037+  /**
1038+   *  da.controller.Navigation.registerColumn(name, filters, column) -> undefined
1039+   *  - name (String): name of the column.
1040+   *  - filters (Array): names of the columns which can accept filter created (with [[da.ui.NavigationColumn#createFilter]]) by this one.
1041+   *  - column (da.ui.NavigationColumn): column class.
1042+   * 
1043+   *  `name` (renamed to `title`) and `filters` will be added to `column` as static methods.
1044+   **/
1045+  registerColumn: function (name, filters, col) {
1046+    col.extend({
1047+      title: name,
1048+      filters: filters || []
1049+    });
1050+   
1051+    this.columns[name] = col;
1052+    if(name !== "Root")
1053+      this.columns.Root.filters.push(name);
1054+  }
1055+};
1056+
1057+da.controller.Navigation = Navigation;
1058+da.app.addEvent("ready", function () {
1059+  Navigation.initialize();
1060+});
1061+
1062+//#require "controllers/default_columns.js"
1063+
1064+da.app.fireEvent("ready.controller.Navigation", [], 1);
1065+})();
1066addfile ./contrib/musicplayer/src/controllers/Player.js
1067hunk ./contrib/musicplayer/src/controllers/Player.js 1
1068+//#require "libs/vendor/soundmanager/script/soundmanager2.js"
1069+
1070+(function () {
1071+/** section: Controllers
1072+ *  class Player
1073+ * 
1074+ *  Player interface and playlist managment class.
1075+ *
1076+ *  #### Notes
1077+ *  This class is private.
1078+ *  Public interface is provided through [[da.controller.Player]].
1079+ **/
1080+var Player = {
1081+  /**
1082+   *  new Player()
1083+   *  Sets up soundManager2 and initializes player's interface.
1084+   **/
1085+  initialize: function () {
1086+    var path = location.protocol + "//" + location.host + location.pathname;
1087+    $extend(soundManager, {
1088+      useHTML5Audio:  false,
1089+      url:            path + 'resources/flash/',
1090+      debugMode:      false,
1091+      debugFlash:     false
1092+    });
1093+   
1094+    soundManager.onready(function () {
1095+      da.app.startup.checkpoint("soundmanager");
1096+    });
1097+  }
1098+};
1099+
1100+Player.initialize();
1101+
1102+/**
1103+ * da.controller.Player
1104+ **/
1105+da.controller.Player = {
1106+  /**
1107+   *  da.controller.Player.play([uri]) -> Boolean
1108+   *  - uri (String): location of the audio.
1109+   *
1110+   *  If `uri` is omitted and there is paused playback, then the paused
1111+   *  file will resume playing.
1112+   **/
1113+  play: function (uri) { return false },
1114
1115+  /**
1116+   *  da.controller.Player.pause() -> Boolean
1117+   * 
1118+   *  Pauses the playback (if any).
1119+   **/
1120+  pause: function () { return false },
1121
1122+  /**
1123+   *  da.controller.Player.queue(uri) -> Boolean
1124+   *  - uri (String): location of the audio file.
1125+   * 
1126+   *  Adds file to the play queue and plays it as soon as currently playing
1127+   *  file finishes playing (if any).
1128+   **/
1129+  queue: function (uri) { return false }
1130+};
1131+
1132+da.app.fireEvent("ready.controller.Player", [], 1);
1133+
1134+})();
1135addfile ./contrib/musicplayer/src/controllers/Settings.js
1136hunk ./contrib/musicplayer/src/controllers/Settings.js 1
1137+//#require "doctemplates/Setting.js"
1138+//#require "libs/ui/NavigationColumn.js"
1139+//#require "libs/ui/Dialog.js"
1140+
1141+(function () {
1142+/** section: Controllers
1143+ *  class Settings
1144+ *
1145+ *  #### Notes
1146+ *  This is private class.
1147+ *  Public interface is accessible via [[da.controller.Settings]].
1148+ **/
1149+   
1150+var Dialog = da.ui.Dialog,
1151+    Setting = da.db.DocumentTemplate.Setting;
1152+
1153+var GROUPS = [{
1154+    id: "caps",
1155+    title: "Caps",
1156+    description: "Tahoe caps for your music and configuration files."
1157+  }, {
1158+    id: "lastfm",
1159+    title: "Last.fm",
1160+    description: 'Share the music your are listening to with the world via <a href="http://last.fm" target="_blank">Last.fm</a>.'
1161+  }
1162+];
1163+
1164+// Renderers are used render interface elements for each setting (input boxes, checkboxes etc.)
1165+// Settings and renderers are bound together via "representAs" property which
1166+// defaults to "text" for each setting.
1167+// All renderer has to do is to renturn a DIV element with "setting_box" CSS class
1168+// which contains an element with "setting_<setting name>" element.
1169+// That same element will be passed to the matching serializer.
1170+
1171+var RENDERERS = {
1172+  _label: function (setting, details) {
1173+    var container = new Element("div", {
1174+      "class": "setting_box"
1175+    });
1176+    return container.grab(new Element("label", {
1177+      text: details.title + ":",
1178+      "for": "setting_" + setting.id
1179+    }));
1180+  },
1181
1182+  text: function (setting, details) {
1183+    return this._label(setting, details).grab(new Element("input", {
1184+      type: "text",
1185+      id: "setting_" + setting.id,
1186+      value: setting.get("value")
1187+   }));
1188+  },
1189
1190+  password: function (setting, details) {
1191+    var text = this.text(setting, details);
1192+    text.getElement("input").type = "password";
1193+    return text;
1194+  },
1195
1196+  checkbox: function (setting, details) {
1197+    var control = this._label(setting, details);
1198+    control.getElement("label").empty().grab(new Element("input", {
1199+      id: "setting_" + setting.id,
1200+      type: "checkbox"
1201+    }));
1202+    control.getElement("input").checked = setting.get("value");
1203+    control.grab(new Element("label", {
1204+      text: details.title,
1205+      "class": "no_indent",
1206+      "for": "setting_" + setting.id
1207+    }));
1208+    return control;
1209+  }
1210+};
1211+RENDERERS.numeric = RENDERERS.text;
1212+
1213+// Serializers do the opposite job of the one that renderers do,
1214+// they take an element and return its value.
1215+var SERIALIZERS = {
1216+  text: function (input) {
1217+    return input.value;
1218+  },
1219
1220+  password: function (input) {
1221+    return input.value;
1222+  },
1223
1224+  numeric: function (input) {
1225+    return +input.value;
1226+  },
1227
1228+  checkbox: function (input) {
1229+    return input.checked;
1230+  }
1231+};
1232+
1233+var Settings = {
1234+  initialize: function () {
1235+    this.dialog = new Dialog({
1236+      title: "Settings",
1237+      html:  new Element("div", {id: "settings"}),
1238+      hideOnOutsideClick: false
1239+    });
1240+    this._el = $("settings");
1241+    this.column = new GroupsColumn({
1242+      parentElement: this._el
1243+    });
1244+   
1245+    var select_message = new Element("div", {
1246+      html: "Click on a group on the left.",
1247+      "class": "message"
1248+    });
1249+    this._controls = new Element("div", {id: "settings_controls"});
1250+    this._controls.grab(select_message);
1251+    this._el.grab(this._controls);
1252+
1253+    this.initialized = true;
1254+  },
1255+
1256+  /**
1257+   *  Settings.show() -> this
1258+   *  Shows the settings panel.
1259+   **/
1260+  show: function () {
1261+    this.dialog.show();
1262+    if(!this._adjusted_height) {
1263+      this._title_height = this._el.getElement(".dialog_title").getHeight();
1264+      this.column.toElement().setStyle("height", 300 - this._title_height);
1265+      this._controls.style.height = (300 - this._title_height) + "px";
1266+      this._adjusted_height = true;
1267+    }
1268+   
1269+    return this;
1270+  },
1271+
1272+  /**
1273+   *  Settings.hide() -> this
1274+   *  Hides the settings panel.
1275+   **/
1276+  hide: function () {
1277+    this.dialog.hide();
1278+    return this;
1279+  },
1280+
1281+  /**
1282+   *  Settings.renderGroup(groupName) -> this
1283+   *  - groupName (String) name of the settings group whose panel
1284+   *    is about to be rendered.
1285+   **/
1286+  renderGroup: function (group) {
1287+    Setting.find({
1288+      properties: {group_id: group.id},
1289+      onSuccess: function (settings) {
1290+        Settings.renderSettings(group.value, settings);
1291+      }
1292+    });
1293+  },
1294+
1295+  /**
1296+   *  Settings.renderSettings(settings) -> false | this
1297+   *  - settings ([Settin]): settings for which controls need to be rendered.
1298+   *
1299+   *  Calls the rendering functions for each setting.
1300+   * 
1301+   **/
1302+  renderSettings: function (group, settings) {
1303+    if(!settings.length)
1304+      return false;   
1305+    if(this._controls)
1306+      this._controls.empty();
1307+
1308+    settings.sort(positionSort);
1309+    var container = new Element("div"),
1310+        header    = new Element("p", {
1311+          html: group.description,
1312+          "class": "settings_header"
1313+        }),
1314+        footer    = new Element("div", {"class": "settings_footer no_selection"}),
1315+        apply_button = new Element("input", {
1316+          type: "button",
1317+          value: "Apply",
1318+          id: "save_settings",
1319+          events: {click: function () { Settings.save() }}
1320+        }),
1321+        revert_button = new Element("input", {
1322+          type: "button",
1323+          value: "Revert",
1324+          id: "revert_settings",
1325+          events: {click: function () { Settings.renderSettings(group, settings) }}
1326+        }),
1327+        settings_el = new Element("form");
1328+
1329+    container.grab(header);
1330+
1331+    var n = settings.length, setting, details;
1332+    while(n--) {
1333+      setting = settings[n];
1334+      details = Setting.getDetails(setting.id);     
1335+      RENDERERS[details.representAs](setting, details).inject(settings_el, "top");
1336+    }
1337
1338+    footer.adopt(revert_button, apply_button);   
1339+    container.adopt(settings_el, footer);
1340+    this._controls.grab(container);
1341+    return this;
1342+  },
1343
1344+  save: function () {
1345+    var settings = this.serialize();
1346+    for(var id in settings)
1347+      Setting.findFirst({
1348+        properties: {id: id},
1349+        onSuccess: function (setting) {
1350+          setting.update({value: settings[id]});
1351+        }
1352+      });
1353+  },
1354
1355+  serialize: function () {
1356+    var values = this._controls.getElement("form").getElements("input[id^=setting_]"),
1357+        serialized = {},
1358+        // in combo with el.id.slice is approx. x10 faster
1359+        // than el.id.split("setting_")[1]
1360+        setting_l = "setting_".length,
1361+        n = values.length;
1362
1363+    while(n--) {
1364+      var el = values[n],
1365+          setting_name = el.id.slice(setting_l),
1366+          details = Setting.getDetails(setting_name);
1367+      serialized[setting_name] = SERIALIZERS[details.representAs](el);
1368+    }
1369+   
1370+    return serialized;
1371+  }
1372+};
1373+$extend(Settings, new Events());
1374+
1375+function positionSort(a, b) {
1376+  a = Setting.getDetails(a.id).position;
1377+  b = Setting.getDetails(b.id).position;
1378
1379+  return (a < b) ? -1 : ((a > b) ? 1 : 0);
1380+}
1381+
1382+var GroupsColumn = new Class({
1383+  Extends: da.ui.NavigationColumn,
1384+
1385+  view: null,
1386+
1387+  initialize: function (options) {
1388+    options.totalCount = GROUPS.length;
1389+    this.parent(options);
1390+   
1391+    this.addEvent("click", function (item) {
1392+      Settings.renderGroup(item);
1393+    });
1394+  },
1395+
1396+  getItem: function (n) {
1397+    var group = GROUPS[n];
1398+    return {id: group.id, value: group};
1399+  }
1400+});
1401+
1402+/**
1403+ * da.controller.Settings
1404+ *
1405+ * Public interface of the settings controller.
1406+ **/
1407+da.controller.Settings = {
1408+  /**
1409+   *  da.controller.Settings.registerGroup(config) -> this
1410+   *  - config.id (String): name of group.
1411+   *  - config.title (String): human-friendly name of the group.
1412+   *  - config.description (String): brief explanation of what this group is for.
1413+   *    The description will be displayed at the top of settings dialog.
1414+   **/
1415+  registerGroup: function (config) {
1416+    GROUPS[name] = config;
1417+    return this;
1418+  },
1419+
1420+  /**
1421+   *  da.controller.Settings.addRenderer(name, renderer) -> this
1422+   *  - name (String): name of the renderer. [[da.db.DocumentTemplate.Setting]] uses this in `representAs` property.
1423+   *  - renderer (Function): function which renderes specific setting.
1424+   *
1425+   *  As first argument `renderer` function takes [[Setting]] object,
1426+   *  while the second one is the result of [[da.db.DocumentTemplate.Setting.getDetails]].
1427+   *
1428+   *  The function *must* return an [[Element]] with `setting_box` CSS class name.
1429+   *  The very same element *must* contain another element with `setting_<setting id>` id property.
1430+   *  That element will be passed to the serializer function.
1431+   *
1432+   *  #### Default renderers
1433+   *  * `text`
1434+   *  * `numeric` (same as `text`, the only difference is in serializer)
1435+   *  * `password`
1436+   *  * `checkbox`
1437+   **/
1438+  addRenderer: function (name, fn) {
1439+    if(!(name in RENDERERS))
1440+      RENDERERS[name] = fn;
1441+   
1442+    return this;
1443+  },
1444
1445+  /**
1446+   *  da.controller.Settings.addSerializer(name, serializer) -> this
1447+   *  - name (String): name of the serializer. Usually the same name used by matching renderer.
1448+   *  - serializer (Function): function which returns value stored by rendered UI controls.
1449+   *    Function takes exactly one argument, the `setting_<setting id>` element.
1450+   **/
1451+  addSerializer: function (name, serializer) {
1452+    if(!(name in SERIALIZERS))
1453+      SERIALIZERS[name] = serializer;
1454+   
1455+    return this; 
1456+  },
1457
1458+  /**
1459+   *  da.controller.Settings.show() -> undefined
1460+   *
1461+   *  Shows the settings dialog.
1462+   **/
1463+  show: function () {
1464+    if(!Settings.initialized)
1465+      Settings.initialize();
1466+   
1467+    Settings.show();
1468+  },
1469
1470+  /**
1471+   *  da.controller.Settings.hide() -> undefined
1472+   *
1473+   *  Hides the settings dialog.
1474+   *  Changes to the settings are not automatically saved when dialog
1475+   *  is dismissed.
1476+   **/
1477+  hide: function () {
1478+    Settings.hide();
1479+  },
1480
1481+  /**
1482+   *  da.controller.Settings.showGroup(group) -> undefined
1483+   *  - group (String): group's id.
1484+   **/
1485+  showGroup: function (group) {
1486+    this.show();
1487+    var n = GROUPS.length;
1488+    while(n--)
1489+      if(GROUPS[n].id === group)
1490+        break;
1491+   
1492+    Settings.renderGroup({id: group, value: GROUPS[n+1]});
1493+  }
1494+};
1495+
1496+da.app.fireEvent("ready.controller.Settings", [], 1);
1497+})();
1498+
1499addfile ./contrib/musicplayer/src/controllers/controllers.js
1500hunk ./contrib/musicplayer/src/controllers/controllers.js 1
1501+/**
1502+ *  == Controllers ==
1503+ * 
1504+ *  Controller classes control "background" jobs and user interface.
1505+ **/
1506+
1507+/** section: Controllers
1508+ * da.controller
1509+ **/
1510+if(typeof da.controller === "undefined")
1511+  da.controller = {};
1512+
1513+//#require "controllers/Navigation.js"
1514+//#require "controllers/Player.js"
1515+//#require "controllers/Settings.js"
1516+//#require "controllers/CollectionScanner.js"
1517addfile ./contrib/musicplayer/src/controllers/default_columns.js
1518hunk ./contrib/musicplayer/src/controllers/default_columns.js 1
1519+//#require "libs/ui/NavigationColumn.js"
1520hunk ./contrib/musicplayer/src/controllers/default_columns.js 3
1521+(function () {
1522+var Navigation = da.controller.Navigation,
1523+    NavigationColumn = da.ui.NavigationColumn;
1524+
1525+/** section: Controller
1526+ *  class da.controller.Navigation.columns.Root < da.ui.NavigationColumn
1527+ *  filters: All filters provided by other columns.
1528+ * 
1529+ *  The root column which provides root menu.
1530+ *  To access the root menu use:
1531+ * 
1532+ *        da.controller.Navigation.columns.Root.menu
1533+ **/
1534+Navigation.registerColumn("Root", [], new Class({
1535+  Extends: NavigationColumn,
1536
1537+  title: "Root",
1538+  view: null,
1539
1540+  initialize: function (options) {
1541+    this._data = Navigation.columns.Root.filters;
1542+    this.parent($extend(options, {
1543+      totalCount: 0 // this._data.length
1544+    }));
1545+    this.render();
1546+    this.options.parentElement.style.display = "none";
1547+  },
1548
1549+  getItem: function (index) {
1550+    return {id: index, key: index, value: {title: this._data[index]}};
1551+  }
1552+}));
1553+
1554+/**
1555+ *  class da.controller.Navigation.columns.Artists < da.ui.NavigationColumn
1556+ *  filters: [[da.controller.Navigation.columns.Albums]], [[da.controller.Navigation.columns.Songs]]
1557+ * 
1558+ *  Displays artists.
1559+ **/
1560+var the_regex = /^the\s*/i;
1561+Navigation.registerColumn("Artists", ["Albums", "Songs"], new Class({
1562+  Extends: NavigationColumn,
1563
1564+  view: {
1565+    id: "artists_column",
1566+    map: function (doc, emit) {
1567+      // If there are no documents in the DB this function
1568+      // will be called with "undefined" as first argument
1569+      if(!doc) return;
1570+
1571+      if(doc.type === "Artist")
1572+        emit(doc.id, {
1573+          title: doc.title
1574+        });
1575+    }
1576+  },
1577
1578+  createFilter: function (item) {
1579+    return {artist_id: item.id};
1580+  },
1581
1582+  compareFunction: function (a, b) {
1583+    a = a && a.value.title ? a.value.title.split(the_regex).slice(-1) : a;
1584+    b = b && b.value.title ? b.value.title.split(the_regex).slice(-1) : b;
1585+   
1586+    if(a < b) return -1;
1587+    if(a > b) return 1;
1588+    return 0;
1589+  }
1590+}));
1591+
1592+/**
1593+ *  class da.controller.Navigation.columns.Albums < da.ui.NavigationColumn
1594+ *  filters: [[da.controller.Navigation.columns.Songs]]
1595+ * 
1596+ *  Displays albums.
1597+ **/
1598+Navigation.registerColumn("Albums", ["Songs"], new Class({
1599+  Extends: NavigationColumn,
1600+
1601+  // We can't reuse "Album" view because of #_passesFilter().
1602+  view: {
1603+    id: "albums_column",
1604+   
1605+    map: function (doc, emit) {
1606+      if(!doc || !this._passesFilter(doc)) return;
1607+
1608+      if(doc.type === "Album")
1609+        emit(doc.id, {
1610+          title: doc.title
1611+        });
1612+    }
1613+  },
1614
1615+  createFilter: function (item) {
1616+    return {album_id: item.id};
1617+  }
1618+}));
1619+
1620+/**
1621+ *  class da.controller.Navigation.columns.Songs < da.ui.NavigationColumn
1622+ *  filters: none
1623+ * 
1624+ *  Displays songs.
1625+ **/
1626+Navigation.registerColumn("Songs", [], new Class({
1627+  Extends: NavigationColumn,
1628
1629+  initialize: function (options) {
1630+    this.parent(options);
1631+   
1632+    this._el.style.width = "300px";
1633+   
1634+    this.addEvent("click", function (item) {
1635+      if(this.sound)
1636+        soundManager.stop(item.id);
1637+
1638+      this.sound = soundManager.createSound({
1639+        id: item.id,
1640+        url: "/uri/" + encodeURIComponent(item.id),
1641+        autoLoad: true,
1642+        onload: function () {
1643+          this.play();
1644+        }
1645+      });
1646+    }.bind(this));
1647+  },
1648
1649+  view: {
1650+    id: "songs_column",
1651+    map: function (doc, emit) {
1652+      if(!doc || !this._passesFilter(doc)) return;
1653+
1654+      if(doc.type === "Song" && doc.title)
1655+        emit(doc.title, {
1656+          title: doc.title,
1657+          subtitle: doc.track,
1658+          track: doc.track
1659+        });
1660+    }
1661+  },
1662
1663+  renderItem: function (index) {
1664+    var item = this.getItem(index).value,
1665+        el = new Element("a", {href: "#", title: item.title});
1666+   
1667+    el.grab(new Element("span", {html: item.title, "class": "title"}));
1668+   
1669+    return el;
1670+  },
1671
1672+  compareFunction: function (a, b) {
1673+    a = a && a.value ? a.value.track : a;
1674+    b = b && b.value ? b.value.track : b;
1675+   
1676+    if(a < b) return -1;
1677+    if(a > b) return 1;
1678+    return 0;
1679+  }
1680+}));
1681+
1682+})();
1683adddir ./contrib/musicplayer/src/doctemplates
1684addfile ./contrib/musicplayer/src/doctemplates/Album.js
1685hunk ./contrib/musicplayer/src/doctemplates/Album.js 1
1686+//#require "libs/db/DocumentTemplate.js"
1687+/**
1688+ *  class da.db.DocumentTemplate.Album < da.db.DocumentTemplate
1689+ *  hasMany: [[da.db.DocumentTemplate.Song]]
1690+ *  belongsTo: [[da.db.DocumentTemplate.Artist]]
1691+ * 
1692+ *  #### Standard properties
1693+ *  * `title` - name of the album
1694+ **/
1695+
1696+(function () {
1697+var DocumentTemplate = da.db.DocumentTemplate;
1698+
1699+DocumentTemplate.registerType("Album", new Class({
1700+  Extends: DocumentTemplate,
1701
1702+  hasMany: {
1703+    songs: "Song"
1704+  },
1705
1706+  belongsTo: {
1707+    artist: "Artist"
1708+  }
1709+}));
1710+
1711+})();
1712addfile ./contrib/musicplayer/src/doctemplates/Artist.js
1713hunk ./contrib/musicplayer/src/doctemplates/Artist.js 1
1714+//#require "libs/db/DocumentTemplate.js"
1715+/**
1716+ *  class da.db.DocumentTemplate.Artist < da.db.DocumentTemplate
1717+ *  hasMany: [[da.db.DocumentTemplate.Song]]
1718+ *  belongsTo: [[da.db.DocumentTemplate.Artist]]
1719+ *
1720+ *  #### Standard properties
1721+ *  * `title` - name of the artist
1722+ *
1723+ **/
1724+(function () {
1725+var DocumentTemplate = da.db.DocumentTemplate;
1726+
1727+DocumentTemplate.registerType("Artist", new Class({
1728+  Extends: DocumentTemplate,
1729
1730+  hasMany: {
1731+    songs: "Song"
1732+  },
1733
1734+  belongsTo: {
1735+    artist: "Artist"
1736+  }
1737+}));
1738+
1739+})();
1740addfile ./contrib/musicplayer/src/doctemplates/Setting.js
1741hunk ./contrib/musicplayer/src/doctemplates/Setting.js 1
1742+(function () {
1743+var DocumentTemplate = da.db.DocumentTemplate,
1744+    // We are separating the actual setting values from
1745+    // information needed to display the UI controls.
1746+    SETTINGS = {};
1747+
1748+/**
1749+ *  class da.db.DocumentTemplate.Setting < da.db.DocumentTemplate
1750+ * 
1751+ *  Class for represeting settings.
1752+ * 
1753+ *  #### Example
1754+ *      da.db.DocumentTemplate.Setting.register({
1755+ *        id:           "volume",
1756+ *        group_id:     "general",
1757+ *        representAs:  "Number",
1758+ *
1759+ *        title:        "Volume",
1760+ *        help:         "Configure the volume",
1761+ *        value:        64
1762+ *      });
1763+ **/
1764+
1765+var Setting = new Class({
1766+  Extends: DocumentTemplate
1767+});
1768+DocumentTemplate.registerType("Setting", da.db.SETTINGS, Setting);
1769+
1770+Setting.extend({
1771+  /**
1772+   *  da.db.DocumentTemplate.Setting.register(template) -> undefined
1773+   *  - template.id (String): ID of the setting.
1774+   *  - template.group_id (String | Number): ID of the group to which setting belongs to.
1775+   *  - template.representAs (String): type of the data this setting represents. ex. `text`, `password`.
1776+   *  - template.title (String): human-friendly name of the setting.
1777+   *  - template.help (String): a semi-long description of what this setting is used for.
1778+   *  - template.value (String | Number | Object): default value.
1779+   *  - template.hidden (Boolean): if `true`, the setting will not be displayed in settings dialog.
1780+   *    Defaults to `false`.
1781+   *  - template.position (Number): position in the list.
1782+   *   
1783+   *  For list of possible `template.representAs` values see [[Settings.addRenderer]] for details.
1784+   **/
1785+  register: function (template) {
1786+    SETTINGS[template.id] = {
1787+      title: template.title,
1788+      help: template.help,
1789+      representAs: template.representAs || "text",
1790+      position: typeof template.position === "number" ? template.position : -1
1791+    };
1792+
1793+    this.findOrCreate({
1794+      properties: {id: template.id},
1795+      onSuccess: function (doc, was_created) {
1796+        if(was_created)
1797+          doc.update({   
1798+            group_id: template.group_id,
1799+            value:    template.value
1800+          });
1801+      }
1802+    });
1803+  },
1804
1805+  /**
1806+   *  da.db.DocumentTemplate.Setting.findInGroup(group, callback) -> undefined
1807+   *  - group (String | Number): ID of the group.
1808+   *  - callback (Function): function called with all found settings.
1809+   **/
1810+  findInGroup: function (group, callback) {
1811+    this.find({
1812+      properties: {group_id: group},
1813+      onSuccess: callback,
1814+      onFailure: callback
1815+    });
1816+  },
1817+
1818+  /**
1819+   *  da.db.DocumentTemplate.Setting.getDetails(id) -> Object
1820+   *  - id (String | Number): id of the setting.
1821+   *
1822+   *  Returns presentation-related details about the given setting.
1823+   *  These details include `title`, `help` and `data` properties given to [[da.db.DocumentTemplate.Setting.register]].
1824+   **/
1825+  getDetails: function (id) {
1826+    return SETTINGS[id];
1827+  }
1828+});
1829+
1830+Setting.register({
1831+  id:           "music_cap",
1832+  group_id:     "caps",
1833+  representAs:  "text",
1834+  title:        "Music cap",
1835+  help:         "Tahoe cap for the root dirnode in which all your music files are.",
1836+  value:        ""
1837+});
1838+
1839+Setting.register({
1840+  id:           "settings_cap",
1841+  group_id:     "caps",
1842+  representAs:  "text",
1843+  title:        "Settings cap",
1844+  help:         "Tahoe read-write cap to the dirnode in which settings will be kept.",
1845+  value:        ""
1846+});
1847+
1848+Setting.register({
1849+  id:           "lastfm_enabled",
1850+  group_id:     "lastfm",
1851+  representAs:  "checkbox",
1852+  title:        "Enable Last.fm scrobbler",
1853+  help:         "Enable this if you whish to share music your are listening to with others.",
1854+  value:        false,
1855+  position:     0
1856+});
1857+
1858+Setting.register({
1859+  id:           "lastfm_username",
1860+  group_id:     "lastfm",
1861+  representAs:  "text",
1862+  title:        "Username",
1863+  help:         "Type in your Last.fm username.",
1864+  value:        "",
1865+  position:     1
1866+});
1867+
1868+Setting.register({
1869+  id:           "lastfm_password",
1870+  group_id:     "lastfm",
1871+  representAs:  "password",
1872+  title:        "Password",
1873+  help:         "Write down your Last.fm password.",
1874+  value:        "",
1875+  position:     2
1876+});
1877+
1878+})();
1879addfile ./contrib/musicplayer/src/doctemplates/Song.js
1880hunk ./contrib/musicplayer/src/doctemplates/Song.js 1
1881+//#require "libs/db/DocumentTemplate.js"
1882+
1883+(function () {
1884+var DocumentTemplate = da.db.DocumentTemplate;
1885+
1886+/**
1887+ *  class da.db.DocumentTemplate.Song < da.db.DocumentTemplate
1888+ *  belongsTo: [[da.db.DocumentTemplate.Artist]], [[da.db.DocumentTemplate.Album]]
1889+ * 
1890+ *  #### Standard properties
1891+ *  * `id` - Read-only cap of the file
1892+ *  * `title` - name of the song
1893+ *  * `track` - track number
1894+ *  * `year` - year in which track was published
1895+ *  * `lyrics` - lyrics of the song
1896+ *  * `artist_id` - id of an [[da.db.DocumentTemplate.Artist]]
1897+ *  * `album_id` - id of an [[da.db.DocumentTemplate.Album]]
1898+ *
1899+ **/
1900+
1901+// Defined by ID3 specs:
1902+// http://www.id3.org/id3v2.3.0#head-129376727ebe5309c1de1888987d070288d7c7e7
1903+var GENRES = [
1904+  "Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz",
1905+  "Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno",
1906+  "Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno",
1907+  "Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental",
1908+  "Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk",
1909+  "Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic",
1910+  "Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream",
1911+  "Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk",
1912+  "Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes",
1913+  "Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical",
1914+  "Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion",
1915+  "Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock",
1916+  "Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band",
1917+  "Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music",
1918+  "Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango",
1919+  "Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock",
1920+  "Drum Solo","A capella","Euro-House","Dance Hall"
1921+];
1922+
1923+DocumentTemplate.registerType("Song", new Class({
1924+  Extends: DocumentTemplate,
1925
1926+  belongsTo: {
1927+    artist: "Artist",
1928+    album: "Album"
1929+  },
1930+
1931+  /**
1932+   *  da.db.DocumentTemplate.Song#getGenre() -> String
1933+   *  Returns human-friendly name of the genre.
1934+   **/
1935+  getGenre: function () {
1936+    return GENRES[this.get("genere")];
1937+  }
1938+}));
1939+
1940+})();
1941addfile ./contrib/musicplayer/src/doctemplates/doctemplates.js
1942hunk ./contrib/musicplayer/src/doctemplates/doctemplates.js 1
1943+/**
1944+ *  == DocumentTemplates ==
1945+ * 
1946+ *  Database document templates.
1947+ *
1948+ **/
1949+
1950+//#require "doctemplates/Setting.js"
1951+//#require "doctemplates/Artist.js"
1952+//#require "doctemplates/Album.js"
1953+//#require "doctemplates/Song.js"
1954addfile ./contrib/musicplayer/src/index.html
1955hunk ./contrib/musicplayer/src/index.html 1
1956+<!DOCTYPE html>
1957+<html>
1958+  <head>
1959+    <title>Music Player for Tahoe-LAFS</title>
1960+   
1961+    <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
1962+    <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/>
1963+    <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/>
1964+   
1965+    <script src="js/app.js" type="text/javascript" charset="utf-8"></script>
1966+  </head>
1967+  <body>
1968+    <div id="loader">Loading...</div>
1969+    <div id="panes" style="display:none">
1970+      <div id="navigation_pane"></div>
1971+      <div id="player_pane"></div>
1972+    </div>
1973+  </body>
1974+</html>
1975addfile ./contrib/musicplayer/src/index_devel.html
1976hunk ./contrib/musicplayer/src/index_devel.html 1
1977+<!DOCTYPE html>
1978+<html>
1979+  <head>
1980+    <title>Music Player for Tahoe-LAFS</title>
1981+   
1982+    <link rel="stylesheet" href="resources/css/reset.css" type="text/css" media="screen" charset="utf-8"/>
1983+    <link rel="stylesheet" href="resources/css/text.css" type="text/css" media="screen" charset="utf-8"/>
1984+    <link rel="stylesheet" href="resources/css/app.css" type="text/css" media="screen" charset="utf-8"/>
1985+   
1986+    <script src="libs/vendor/mootools-1.2.4-core-ui.js" type="text/javascript" charset="utf-8"></script>
1987+    <script src="libs/vendor/mootools-1.2.4.4-more.js" type="text/javascript" charset="utf-8"></script>
1988+    <script src="libs/vendor/persist-js/src/persist.js" type="text/javascript" charset="utf-8"></script>
1989+    <script src="libs/vendor/soundmanager/script/soundmanager2.js" type="text/javascript" charset="utf-8"></script>
1990+
1991+    <script type="text/javascript" charset="utf-8">
1992+      this.da = {};
1993+    </script>
1994+
1995+    <script src="libs/db/db.js" type="text/javascript" charset="utf-8"></script>
1996+    <script src="libs/db/PersistStorage.js" type="text/javascript" charset="utf-8"></script>
1997+    <script src="libs/db/BrowserCouch.js" type="text/javascript" charset="utf-8"></script>
1998+    <script src="libs/vendor/Math.uuid.js" type="text/javascript" charset="utf-8"></script>
1999+    <script src="libs/util/util.js" type="text/javascript" charset="utf-8"></script>
2000+    <script src="libs/db/DocumentTemplate.js" type="text/javascript" charset="utf-8"></script>
2001+    <script src="libs/util/Goal.js" type="text/javascript" charset="utf-8"></script>
2002+    <script src="libs/util/BinaryFile.js" type="text/javascript" charset="utf-8"></script>
2003+    <script src="libs/util/ID3.js" type="text/javascript" charset="utf-8"></script>
2004+    <script src="libs/util/ID3v2.js" type="text/javascript" charset="utf-8"></script>
2005+    <script src="libs/util/ID3v1.js" type="text/javascript" charset="utf-8"></script>
2006+
2007+    <script src="Application.js" type="text/javascript" charset="utf-8"></script>
2008+
2009+    <script src="doctemplates/Setting.js" type="text/javascript" charset="utf-8"></script>
2010+    <script src="doctemplates/Artist.js" type="text/javascript" charset="utf-8"></script>
2011+    <script src="doctemplates/Album.js" type="text/javascript" charset="utf-8"></script>
2012+    <script src="doctemplates/Song.js" type="text/javascript" charset="utf-8"></script>
2013+
2014+    <script src="libs/ui/ui.js" type="text/javascript" charset="utf-8"></script>
2015+    <script src="libs/ui/Column.js" type="text/javascript" charset="utf-8"></script>
2016+    <script src="libs/ui/NavigationColumn.js" type="text/javascript" charset="utf-8"></script>
2017+    <script src="libs/ui/Menu.js" type="text/javascript" charset="utf-8"></script>
2018+    <script src="libs/ui/Dialog.js" type="text/javascript" charset="utf-8"></script>
2019+
2020+    <script src="controllers/controllers.js" type="text/javascript" charset="utf-8"></script>
2021+    <script src="controllers/Navigation.js" type="text/javascript" charset="utf-8"></script>
2022+    <script src="controllers/default_columns.js" type="text/javascript" charset="utf-8"></script>
2023+    <script src="controllers/Player.js" type="text/javascript" charset="utf-8"></script>
2024+    <script src="controllers/Settings.js" type="text/javascript" charset="utf-8"></script>
2025+    <script src="controllers/CollectionScanner.js" type="text/javascript" charset="utf-8"></script>
2026+  </head>
2027+  <body>
2028+    <div id="loader">Loading...</div>
2029+    <div id="panes" style="display:none">
2030+      <div id="navigation_pane"></div>
2031+      <div id="player_pane"></div>
2032+    </div>
2033+  </body>
2034+</html>
2035adddir ./contrib/musicplayer/src/libs
2036addfile ./contrib/musicplayer/src/libs/TahoeObject.js
2037hunk ./contrib/musicplayer/src/libs/TahoeObject.js 1
2038+(function () {
2039+/**
2040+ *  == Tahoe ==
2041+ * 
2042+ *  Classes and utility methods for working with Tahoe's [web API](http://tahoe-lafs.org/source/tahoe/trunk/docs/frontends/webapi.txt).
2043+ **/
2044+var CACHE = {};
2045+
2046+/** section: Tahoe
2047+ *  class TahoeObject
2048+ * 
2049+ *  Abstract class representing any Tahoe object - either file or directory.
2050+ **/
2051+var TahoeObject = new Class({
2052+  /**
2053+   *  new TahoeObject(cap[, meta])
2054+   *  - cap (String): cap of the object.
2055+   *  - meta (Object): metadata about the object.
2056+   **/
2057+  initialize: function (uri, meta) {
2058+    this.uri = uri;
2059+    CACHE[uri] = this;
2060+    this._fetched = false;
2061+   
2062+    if(meta)
2063+      this.applyMeta(meta);
2064+  },
2065
2066+  /**
2067+   *  TahoeObject#applyMeta(meta) -> this
2068+   *  - meta (Object): metadata about the object.
2069+   * 
2070+   *  Applies the metadata to current object. If `meta` contains information
2071+   *  of child items, new [[TahoeObject]] instances will be created for those
2072+   *  as well.
2073+   **/
2074+  applyMeta: function (meta) {
2075+    this.type = meta[0];
2076+    var old_children = meta[1].children || {},
2077+        children = [];
2078+
2079+    for(var child_name in old_children) {
2080+      var child = old_children[child_name];
2081+      child[1].objectName = child_name;
2082+      //child[1].type = child[0];
2083+     
2084+      if(CACHE[child[1].ro_uri])
2085+        children.push(CACHE[child[1].ro_uri])
2086+      else
2087+        children.push(new TahoeObject(child[1].ro_uri, child));
2088+    }
2089+
2090+    meta[1].children = children;
2091+    $extend(this, meta[1]);
2092+   
2093+    return this;
2094+  },
2095
2096+  /**
2097+   *  TahoeObject#get([onSuccess][, onFailure]) -> this
2098+   *  - onSuccess (Funcion): called if request succeeds. First argument is `this`.
2099+   *  - onFailure (Function): called if request fails.
2100+   * 
2101+   *  Requests metadata about `this` object.
2102+   **/
2103+  get: function (success, failure) {
2104+    if(this._fetched) {
2105+      (success||$empty)(this);
2106+      return this;
2107+    }
2108+    this._fetched = true;
2109+
2110+    new Request.JSON({
2111+      url: "/uri/" + encodeURIComponent(this.uri),
2112+     
2113+      onSuccess: function (data) {
2114+        this.applyMeta(data);
2115+        (success||$empty)(this);
2116+      }.bind(this),
2117+     
2118+      onFailure: failure || $empty
2119+    }).get({t: "json"});
2120+   
2121+    return this;
2122+  },
2123
2124+  /**
2125+   *  TahoeObject#directories() -> [TahoeObject...]
2126+   *  Returns an [[Array]] of all child directories.
2127+   **/
2128+  directories: function () {
2129+    var children = this.children,
2130+        n = children.length,
2131+        result = [];
2132+   
2133+    while(n--)
2134+      if(children[n].type === "dirnode")
2135+        result.push(children[n]);
2136+   
2137+    return result;
2138+  },
2139
2140+  /**
2141+   *  TahoeObject#files() -> [TahoeObject...]
2142+   *  Returns an [[Array]] of all child files.
2143+   **/
2144+  files: function () {
2145+    var children = this.children,
2146+        n = children.length,
2147+        result = [];
2148+   
2149+    while(n--)
2150+      if(children[n].type === "filenode")
2151+        result.push(children[n]);
2152+   
2153+    return result;
2154+  }
2155+});
2156+window.TahoeObject = TahoeObject;
2157+
2158+})();
2159adddir ./contrib/musicplayer/src/libs/db
2160addfile ./contrib/musicplayer/src/libs/db/BrowserCouch.js
2161hunk ./contrib/musicplayer/src/libs/db/BrowserCouch.js 1
2162+/* ***** BEGIN LICENSE BLOCK *****
2163+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
2164+ *
2165+ * The contents of this file are subject to the Mozilla Public License Version
2166+ * 1.1 (the "License"); you may not use this file except in compliance with
2167+ * the License. You may obtain a copy of the License at
2168+ * http://www.mozilla.org/MPL/
2169+ *
2170+ * Software distributed under the License is distributed on an "AS IS" basis,
2171+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
2172+ * for the specific language governing rights and limitations under the
2173+ * License.
2174+ *
2175+ * The Original Code is Ubiquity.
2176+ *
2177+ * The Initial Developer of the Original Code is Mozilla.
2178+ * Portions created by the Initial Developer are Copyright (C) 2007
2179+ * the Initial Developer. All Rights Reserved.
2180+ *
2181+ * Contributor(s):
2182+ *   Atul Varma <atul@gmozilla.com>
2183+ *
2184+ * Alternatively, the contents of this file may be used under the terms of
2185+ * either the GNU General Public License Version 2 or later (the "GPL"), or
2186+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
2187+ * in which case the provisions of the GPL or the LGPL are applicable instead
2188+ * of those above. If you wish to allow use of your version of this file only
2189+ * under the terms of either the GPL or the LGPL, and not to allow others to
2190+ * use your version of this file under the terms of the MPL, indicate your
2191+ * decision by deleting the provisions above and replace them with the notice
2192+ * and other provisions required by the GPL or the LGPL. If you do not delete
2193+ * the provisions above, a recipient may use your version of this file under
2194+ * the terms of any one of the MPL, the GPL or the LGPL.
2195+ * 
2196+ * ***** END LICENSE BLOCK ***** */
2197+
2198+/*
2199+ *  TODO: Update license block - we've done some changes:
2200+ *    - optimised loops - faster map proccess
2201+ *    - removed LocalStorage and FakeStorage - using PersistStorage instead
2202+ *    - removed ModuleLoader - it's safe to assume JSON is available - speed optimisation
2203+ *    - fixed mapping proccess - if no documents were emitted finish function wouldn't be called
2204+ *    - added live views - map/reduce is only performed on updated documents
2205+ */
2206+
2207+//#require <libs/db/db.js>
2208+
2209+(function () {
2210
2211+/** section: Database
2212+ *  class da.db.BrowserCouch
2213+ * 
2214+ *  Map/Reduce framework for browser.
2215+ * 
2216+ *  #### MapReducer Implementations
2217+ *  MapReducer is a generic interface for any map-reduce
2218+ *  implementation. Any object implementing this interface will need
2219+ *  to be able to work asynchronously, passing back control to the
2220+ *  client at a given interval, so that the client has the ability to
2221+ *  pause/cancel or report progress on the calculation if needed.
2222+ **/
2223+var BrowserCouch = {
2224+  /**
2225+   *  da.db.BrowserCouch.get(name, callback[, storage]) -> DB
2226+   *  - name (String): name of the database.
2227+   *  - callback (Function): called when database is initialized.
2228+   *  - storage (Function): instance of storage class.
2229+   *    Defaults to [[da.db.PersistStorage]].
2230+   **/
2231+  get: function BC_get(name, cb, storage) {
2232+    if (!storage)
2233+      storage = new da.db.PersistStorage(name);
2234+
2235+    new DB({
2236+      name: name,
2237+      storage: storage,
2238+      dict: new Dictionary(),
2239+      onReady: cb
2240+    });
2241+  }
2242+};
2243+
2244+/**
2245+ *  class da.db.BrowserCouch.Dictionary
2246+ * 
2247+ *  Internal representation of the database.
2248+ **/
2249+/**
2250+ *  new da.db.BrowserCouch.Dictionary([object])
2251+ *  - object (Object): initial values
2252+ **/
2253+function Dictionary (object) {
2254+  /**
2255+   *  da.db.BrowserCouch.Dictionary#dict -> Object
2256+   *  The dictionary itself.
2257+   **/
2258+  this.dict = {};
2259+  /**
2260+   *  da.db.BrowserCouch.Dictionary#keys -> Array
2261+   *
2262+   *  Use this property to determine the number of items
2263+   *  in the dictionary.
2264+   *
2265+   *      (new Dictionary({h: 1, e: 1, l: 2, o: 1})).keys.length
2266+   *      // => 4
2267+   *
2268+   **/
2269+  this.keys = [];
2270
2271+  if(object)
2272+    this.unpickle(object);
2273
2274+  return this;
2275+}
2276+
2277+Dictionary.prototype = {
2278+  /**
2279+   *  da.db.BrowserCouch.Dictionary#has(key) -> true | false
2280+   **/
2281+  has: function (key) {
2282+    return (key in this.dict);
2283+  },
2284
2285+  /*
2286+  getKeys: function () {
2287+    return this.keys;
2288+  },
2289+
2290+  get: function (key) {
2291+    return this.dict[key];
2292+  },
2293+  */
2294+
2295+  /**
2296+   *  da.db.BrowserCouch.Dictionary#set(key, value) -> undefined
2297+   **/
2298+  set: function (key, value) {
2299+    if(!(key in this.dict))
2300+      this.keys.push(key);
2301+     
2302+    this.dict[key] = value;
2303+  },
2304
2305+  /**
2306+   *  da.db.BrowserCouch.Dictionary#setDocs(docs) -> undefined
2307+   *  - docs ([Object, ...]): array of objects whose `id` property
2308+   *  will be used as key.
2309+   *
2310+   *  Use this method whenever you have to add more then one
2311+   *  item to the dictionary as it provides better perofrmance over
2312+   *  calling [[da.db.BrowserCouch.Dictionary#set]] over and over.
2313+   **/
2314+  setDocs: function (docs) {
2315+    var n = docs.length,
2316+        newKeys = [];
2317+   
2318+    while(n--) {
2319+      var doc = docs[n], id = doc.id;
2320+      if(!(id in this.dict) && newKeys.indexOf(id) === -1)
2321+        newKeys.push(id);
2322+     
2323+      this.dict[id] = doc;
2324+    }
2325+   
2326+    this.keys = this.keys.concat(newKeys);
2327+  },
2328+
2329+  /**
2330+   *  da.db.BrowserCouch.Dictionary#remove(key) -> undefined
2331+   **/
2332+  remove: function (key) {
2333+    delete this.dict[key];
2334+   
2335+    var keys = this.keys,
2336+        index = keys.indexOf(key),
2337+        keysLength = keys.length;
2338+       
2339+    if(index === 0)
2340+      return this.keys.shift();
2341+    if(index === length - 1)
2342+      return this.keys = keys.slice(0, -1);
2343+   
2344+    this.keys = keys.slice(0, index).concat(keys.slice(index + 1, keysLength));
2345+  },
2346+
2347+  /**
2348+   *  da.db.BrowserCouch.Dictionary#clear() -> undefined
2349+   **/
2350+  clear: function () {
2351+    this.dict = {};
2352+    this.keys = [];
2353+  },
2354
2355+  /**
2356+   *  da.db.BrowserCouch.Dictionary#unpickle(object) -> undefined
2357+   *  - object (Object): `object`'s properties will be replaced with current ones.
2358+   **/
2359+  unpickle: function (obj) {
2360+    if(!obj)
2361+      return;
2362+   
2363+    this.dict = obj;
2364+    this._regenerateKeys();
2365+  },
2366
2367+  _regenerateKeys: function () {
2368+    var keys = [],
2369+        dict = this.dict;
2370+   
2371+    for(var key in dict)
2372+      keys.push(key);
2373+   
2374+    this.keys = keys;
2375+  }
2376+};
2377+
2378+/** section: Database
2379+ *  class DB
2380+ * 
2381+ *  da.db.BrowserCouch database instance.
2382+ **/
2383+var DB = new Class({
2384+  Implements: [Events, Options],
2385
2386+  options: {},
2387+  /**
2388+   *  new DB(options)
2389+   *  - options.name (String)
2390+   *  - options.storage (Object)
2391+   *  - options.dict (Dictionary)
2392+   *  - options.onReady (Function)
2393+   *  fires ready
2394+   **/
2395+  initialize: function (options) {
2396+    this.setOptions(options);
2397+   
2398+    this.name = "BrowserCouch_DB_" + this.options.name;
2399+    this.dict = this.options.dict;
2400+    this.storage = this.options.storage;
2401+    this.views = {};
2402+   
2403+    this.storage.get(this.name, function (obj) {
2404+      this.dict.unpickle(obj);
2405+      this.fireEvent("ready", [this]);
2406+    }.bind(this));
2407+   
2408+    this.addEvent("store", function (docs) {
2409+      var views = this.views,
2410+          dict = new Dictionary();
2411+     
2412+      if($type(docs) === "array")
2413+        dict.setDocs(docs);
2414+      else
2415+        dict.set(docs.id, docs);
2416+     
2417+      for(var view_name in views)
2418+        this.view(views[view_name].options, dict);
2419+      }.bind(this), true);
2420+  },
2421
2422+  /**
2423+   *  DB#commitToStorage(callback) -> undefined
2424+   **/
2425+  commitToStorage: function (callback) {
2426+    if(!callback)
2427+      callback = $empty;
2428+   
2429+    this.storage.put(this.name, this.dict.dict, callback);
2430+  },
2431
2432+  /**
2433+   *  DB#wipe(callback) -> undefined
2434+   **/
2435+  wipe: function wipe(cb) {
2436+    this.dict.clear();
2437+    this.commitToStorage(cb);
2438+    this.views = {};
2439+  },
2440+
2441+  /**
2442+   *  DB#get(id, callback) -> undefined
2443+   *  - id (String): id of the document.
2444+   **/
2445+  get: function get(id, cb) {
2446+    if(this.dict.has(id))
2447+      cb(this.dict.dict[id]);
2448+    else
2449+      cb(null);
2450+  },
2451+
2452+  /**
2453+   *  DB#getLength() -> Number
2454+   *  Size of the database - number of documents.
2455+   **/
2456+  getLength: function () {
2457+    return this.dict.keys.length;
2458+  },
2459+
2460+  /**
2461+   *  DB#put(document, callback) -> undefined
2462+   *  DB#put(documents, callback) -> undefined
2463+   *  - document.id (String | Number): remember to set this property
2464+   *  - documents (Array): array of documents.
2465+   *  fires store
2466+   **/
2467+  put: function (doc, cb) {
2468+    if ($type(doc) === "array") {
2469+      this.dict.setDocs(doc);
2470+      //var n = doc.length, _doc;
2471+      //while(n--) {
2472+      //  _doc = doc[n];
2473+      //  this.dict.set(_doc.id, _doc);
2474+      //}
2475+    } else
2476+      this.dict.set(doc.id, doc);
2477+
2478+    this.commitToStorage(cb);
2479+    this.fireEvent("store", [doc]);
2480+  },
2481
2482+  /**
2483+   *  DB#view(options[, _dict]) -> this
2484+   *  - options.id (String): name of the view. Optional for temporary views.
2485+   *  - options.map (Function): mapping function. First argument is the document,
2486+   *    while second argument is `emit` function.
2487+   *  - options.reduce (Function): reduce function.
2488+   *  - options.finished (Function): called once map/reduce process finishes.
2489+   *  - options.updated (Function): called on each update to the view.
2490+   *     First argument is a view with only new/changed documents.
2491+   *  - options.progress (Function): called between pauses.
2492+   *  - options.chunkSize (Number): number of documents to be processed at once.
2493+   *    Defaults to 50.
2494+   *  - options.mapReducer (Object): MapReducer to be used.
2495+   *    Defaults to [[da.db.SingleThreadedMapReducer]].
2496+   *  - options.temporary (Boolean): if enabled, new updates won't be reported.
2497+   *    (`options.updated` won't be called at all)
2498+   *  - _dict (Dictionary): objects on which proccess will be performed.
2499+   *    Defaults to current database.
2500+   **/
2501+  view: function DB_view(options, dict) {
2502+    if(!options.id && !options.temporary)
2503+      return false;
2504+    if(!options.map)
2505+      return false;
2506+    if(!options.finished)
2507+      return false;
2508+
2509+    if(typeof options.temporary === "undefined")
2510+      options.temporary = false;
2511+    if(options.updated && !this.views[options.id])
2512+      this.addEvent("updated." + options.id, options.updated);
2513+    if(!options.mapReducer)
2514+      options.mapReducer = SingleThreadedMapReducer;
2515+    if(!options.progress)
2516+      options.progress = defaultProgress;
2517+    if(!options.chunkSize)
2518+      options.chunkSize = DEFAULT_CHUNK_SIZE;
2519+     
2520+    var onReduce = function onReduce (rows) {
2521+      this._updateView(options, new ReduceView(rows), rows);
2522+    }.bind(this);
2523+   
2524+    var onMap = function (mapResult) {
2525+      if(!options.reduce)
2526+        this._updateView(options, new MapView(mapResult), mapResult);
2527+      else
2528+        options.mapReducer.reduce(
2529+          options.reduce, mapResult, options.progress, options.chunkSize, onReduce
2530+        );
2531+    }.bind(this);
2532+   
2533+    options.mapReducer.map(
2534+      options.map,
2535+      dict || this.dict,
2536+      options.progress,
2537+      options.chunkSize,
2538+      onMap
2539+    );
2540+   
2541+    return this;
2542+  },
2543
2544+  _updateView: function (options, view, rows) {
2545+    if(options.temporary)
2546+      return options.finished(view);
2547+   
2548+    var id = options.id;
2549+    if(!this.views[id]) {
2550+      this.views[id] = {
2551+        options: options,
2552+        view: view
2553+      };
2554+      options.finished(view);
2555+    } else {
2556+      if(!view.rows.length)
2557+        return this;
2558+
2559+      if(options.reduce) {
2560+        var full_view = this.views[id].view.rows.concat(view.rows),
2561+            rereduce = {},
2562+            reduce = options.reduce,
2563+            n = full_view.length;
2564+       
2565+        while(n--) {
2566+          var row = full_view[n],
2567+              key = row.key;
2568+          if(!rereduce[key])
2569+            rereduce[key] = [row.value];
2570+          else
2571+            rereduce[key].push(row.value);
2572+        }
2573+       
2574+        rows = [];
2575+        for(var key in rereduce)
2576+          rows.push({
2577+            key: key,
2578+            value: reduce(null, rereduce[key], true)
2579+          });
2580+      }
2581+     
2582+      this.views[id].view._include(rows);
2583+      this.fireEvent("updated." + id, [view]);
2584+    }
2585+   
2586+    return this;
2587+  },
2588
2589+  /**
2590+   *  DB#killView(id) -> this
2591+   *  - id (String): name of the view.
2592+   **/
2593+  killView: function (id) {
2594+    delete this.views[id];
2595+    return this;
2596+  }
2597+});
2598+
2599+// Maximum number of items to process before giving the UI a chance
2600+// to breathe.
2601+var DEFAULT_CHUNK_SIZE = 1000;
2602+
2603+// If no progress callback is given, we'll automatically give the
2604+// UI a chance to breathe for this many milliseconds before continuing
2605+// processing.
2606+var DEFAULT_UI_BREATHE_TIME = 50;
2607+
2608+function defaultProgress(phase, percent, resume) {
2609+  window.setTimeout(resume, DEFAULT_UI_BREATHE_TIME);
2610+}
2611+
2612+/**
2613+ *  class ReduceView
2614+ *  Represents the result of map/reduce process.
2615+ **/
2616+/**
2617+ *  new ReduceView(rows) -> this
2618+ *  - rows (Array): value returned by reducer.
2619+ **/
2620+function ReduceView(rows) {
2621+  /**
2622+   *  ReduceView#rows -> Array
2623+   *  Result of the reduce process.
2624+   **/
2625+  this.rows = [];
2626+  var keys = [];
2627+
2628+  this._include = function (newRows) {
2629+    var n = newRows.length;
2630+   
2631+    while(n--) {
2632+      var row = newRows[n];
2633+      if(keys.indexOf(row.key) === -1) {
2634+        this.rows.push(row);
2635+        keys.push(row.key);
2636+      } else {
2637+        this.rows[this.findRow(row.key)] = newRows[n];
2638+      }
2639+    }
2640+   
2641+    this.rows.sort(keySort);
2642+  };
2643
2644+  /**
2645+   *  ReduceView#findRow(key) -> Number
2646+   *  - key (String): key of the row.
2647+   *
2648+   *  Returns position of the row in [[ReduceView#rows]].
2649+   **/
2650+  this.findRow = function (key) {
2651+    return findRowInReducedView(key, rows);
2652+  };
2653
2654+  /**
2655+   *  ReduceView#getRow(key) -> row
2656+   *  - key (String): key of the row.
2657+   **/
2658+  this.getRow = function (key) {
2659+    var row = this.rows[findRowInReducedView(key, rows)];
2660+    return row ? row.value : undefined;
2661+  };
2662
2663+  this._include(rows);
2664+  return this;
2665+}
2666+
2667+function keySort (a, b) {
2668+  a = a.key;
2669+  b = b.key
2670+  if(a < b) return -1;
2671+  if(a > b) return  1;
2672+            return  0;
2673+}
2674+
2675+function findRowInReducedView (key, rows) {
2676+  if(rows.length > 1) {
2677+    var midpoint = Math.floor(rows.length / 2);
2678+    var row = rows[midpoint];
2679+    if(key < row.key)
2680+      return findRowInReducedView(key, rows.slice(0, midpoint));
2681+    if(key > row.key)
2682+      return midpoint + findRowInReducedView(key, rows.slice(midpoint));
2683+    return row.key === key ? midpoint : -1;
2684+  }
2685
2686+  return rows[0].key === key ? 0 : -1;
2687+}
2688+
2689+/**
2690+ *  class MapView
2691+ *  Represents the result of map/reduce process.
2692+ **/
2693+/**
2694+ *  new MapView(rows) -> this
2695+ *  - rows (Array): value returned by mapper.
2696+ **/
2697+function MapView (mapResult) {
2698+  /**
2699+   *  MapView#rows -> Object
2700+   *  Result of the mapping process.
2701+   **/
2702+  this.rows = [];
2703+  var keyRows = [];
2704
2705+  this._include = function (mapResult) {
2706+    var mapKeys = mapResult.keys,
2707+        mapDict = mapResult.dict;
2708+   
2709+    for(var i = 0, ii = mapKeys.length; i < ii; i++) {
2710+      var key = mapKeys[i],
2711+          ki = this.findRow(key),
2712+          has_key = ki !== -1,
2713+          item = mapDict[key],
2714+          j = item.keys.length,
2715+          newRows = new Array(j);
2716+     
2717+      if(has_key && this.rows[ki]) {
2718+        this.rows[ki].value = item.values.shift();
2719+        item.keys.shift();
2720+        j--;
2721+      } //else
2722+        //keyRows.push({key: key, pos: this.rows.length});
2723+     
2724+      while(j--)
2725+        newRows[j] = {
2726+          id: item.keys[j],
2727+          key: key,
2728+          value: item.values[j]
2729+        };
2730+     
2731+      if(has_key)
2732+        newRows.shift();
2733+      this.rows = this.rows.concat(newRows);
2734+    }
2735+   
2736+    this.rows.sort(idSort);
2737+   
2738+    var keys = [];
2739+    keyRows = [];
2740+    for(var n = 0, m = this.rows.length; n < m; n++) {
2741+      var key = this.rows[n].key;
2742+      if(keys.indexOf(key) === -1)
2743+        keyRows.push({
2744+          key: key,
2745+          pos: keys.push(key) - 1
2746+        });
2747+    }
2748+   
2749+    //delete keys;
2750+  };
2751
2752+  /**
2753+   *  MapView#findRow(key) -> Number
2754+   *  - key (String): key of the row.
2755+   *
2756+   *  Returns position of the row in [[MapView#rows]].
2757+   **/
2758+  this.findRow = function MV_findRow (key) {
2759+    return findRowInMappedView(key, keyRows);
2760+  };
2761
2762+  /**
2763+   *  MapView#getRow(key) -> row
2764+   *  - key (String): key of the row.
2765+   *
2766+   *  Returns row's value, ie. it's a shortcut for:
2767+   *      this.rows[this.findRow(key)].value
2768+   **/
2769+  this.getRow = function MV_findRow (key) {
2770+    var row = this.rows[findRowInMappedView(key, keyRows)];
2771+    return row ? row.value : undefined;
2772+  };
2773
2774+  this._include(mapResult);
2775
2776+  return this;
2777+}
2778+
2779+function idSort (a, b) {
2780+  a = a.id;
2781+  b = b.id;
2782
2783+  if(a < b) return -1;
2784+  if(a > b) return  1;
2785+            return  0;
2786+}
2787+
2788+function findRowInMappedView (key, keyRows) {
2789+  if (keyRows.length > 1) {
2790+    var midpoint = Math.floor(keyRows.length / 2);
2791+    var keyRow = keyRows[midpoint];
2792+    if (key < keyRow.key)
2793+      return findRowInMappedView(key, keyRows.slice(0, midpoint));
2794+    if (key > keyRow.key)
2795+      return findRowInMappedView(key, keyRows.slice(midpoint));
2796+    return keyRow ? keyRow.pos : -1;
2797+  } else
2798+    return (keyRows[0] && keyRows[0].key === key) ? keyRows[0].pos : -1;
2799+}
2800+
2801+/** section: Database
2802+ *  class WebWorkerMapReducer
2803+ * 
2804+ *  A MapReducer that uses [Web Workers](https://developer.mozilla.org/En/Using_DOM_workers)
2805+ *  for its implementation, allowing the client to take advantage of
2806+ *  multiple processor cores and potentially decouple the map-reduce
2807+ *  calculation from the user interface.
2808+ * 
2809+ *  The script run by spawned Web Workers is [[MapReduceWorker]].
2810+ **/
2811+
2812+/**
2813+ *  new WebWorkerMapReducer(numWorkers[, worker])
2814+ *  - numWorkers (Number): number of workers.
2815+ *  - worker (Object): reference to Web worker implementation. Defaults to `window.Worker`.
2816+ **/
2817+function WebWorkerMapReducer(numWorkers, Worker) {
2818+  if (!Worker)
2819+    Worker = window.Worker;
2820+
2821+  var pool = [];
2822+
2823+  function MapWorker(id) {
2824+    var worker = new Worker('js/workers/map-reducer.js');
2825+    var onDone;
2826+
2827+    worker.onmessage = function(event) {
2828+      onDone(event.data);
2829+    };
2830+
2831+    this.id = id;
2832+    this.map = function MW_map(map, dict, cb) {
2833+      onDone = cb;
2834+      worker.postMessage({map: map.toString(), dict: dict});
2835+    };
2836+  }
2837+
2838+  for (var i = 0; i < numWorkers; i++)
2839+    pool.push(new MapWorker(i));
2840+
2841+  this.map = function WWMR_map(map, dict, progress, chunkSize, finished) {
2842+    var keys = dict.keys,
2843+        size = keys.length,
2844+        workersDone = 0,
2845+        mapDict = {};
2846+
2847+    function getNextChunk() {
2848+      if (keys.length) {
2849+        var chunkKeys = keys.slice(0, chunkSize),
2850+            chunk = {},
2851+            n = chunkKeys.length;
2852+       
2853+        keys = keys.slice(chunkSize);
2854+        var key;
2855+        while(n--) {
2856+          key = chunkKeys[n];
2857+          chunk[key] = dict.dict[key];
2858+        }
2859+        return chunk;
2860+//        for (var i = 0, ii = chunkKeys.length; i < ii; i++)
2861+//          chunk[chunkKeys[i]] = dict.dict[chunkKeys[i]];
2862+
2863+      } else
2864+        return null;
2865+    }
2866+
2867+    function nextJob(mapWorker) {
2868+      var chunk = getNextChunk();
2869+      if (chunk) {
2870+        mapWorker.map(
2871+          map,
2872+          chunk,
2873+          function jobDone(aMapDict) {
2874+            for (var name in aMapDict)
2875+              if (name in mapDict) {
2876+                var item = mapDict[name];
2877+                item.keys = item.keys.concat(aMapDict[name].keys);
2878+                item.values = item.values.concat(aMapDict[name].values);
2879+              } else
2880+                mapDict[name] = aMapDict[name];
2881+
2882+            if (keys.length)
2883+              progress("map",
2884+                       (size - keys.length) / size,
2885+                       function() { nextJob(mapWorker); });
2886+            else
2887+              workerDone();
2888+          });
2889+      } else
2890+        workerDone();
2891+    }
2892+
2893+    function workerDone() {
2894+      workersDone += 1;
2895+      if (workersDone == numWorkers)
2896+        allWorkersDone();
2897+    }
2898+
2899+    function allWorkersDone() {
2900+      var mapKeys = [];
2901+      for (var name in mapDict)
2902+        mapKeys.push(name);
2903+      mapKeys.sort();
2904+      finished({dict: mapDict, keys: mapKeys});
2905+    }
2906+
2907+    for (var i = 0; i < numWorkers; i++)
2908+      nextJob(pool[i]);
2909+  };
2910+
2911+  // TODO: Actually implement our own reduce() method here instead
2912+  // of delegating to the single-threaded version.
2913+  this.reduce = SingleThreadedMapReducer.reduce;
2914+};
2915+
2916+/** section: Database
2917+ * da.db.SingleThreadedMapReducer
2918+ *
2919+ * A MapReducer that works on the current thread.
2920+ **/
2921+var SingleThreadedMapReducer = {
2922+  /**
2923+   *  da.db.SingleThreadedMapReducer.map(map, dict, progress, chunkSize, finished) -> undefined
2924+   *  - map (Function): mapping function.
2925+   *  - dict (Object): database documents.
2926+   *  - progress (Function): progress reporting function. Called with `"map"` as first argument.
2927+   *  - chunkSize (Number): number of documents to map at once.
2928+   *  - finished (Function): called once map proccess finishes.
2929+   **/
2930+  map: function STMR_map(map, dict, progress,
2931+                         chunkSize, finished) {
2932+    var mapDict = {},
2933+        keys = dict.keys,
2934+        currDoc;
2935+
2936+    function emit(key, value) {
2937+      // TODO: This assumes that the key will always be
2938+      // an indexable value. We may have to hash the value,
2939+      // though, if it's e.g. an Object.
2940+      var item = mapDict[key];
2941+      if (!item)
2942+        item = mapDict[key] = {keys: [], values: []};
2943+      item.keys.push(currDoc.id);
2944+      item.values.push(value);
2945+    }
2946+
2947+    var i = 0;
2948+
2949+    function continueMap() {
2950+      var iAtStart = i, keysLength = keys.length;
2951+
2952+      if(keysLength > 0)
2953+        do {
2954+          currDoc = dict.dict[keys[i]];
2955+          map(currDoc, emit);
2956+          i++;
2957+        } while (i - iAtStart < chunkSize && i < keysLength);
2958+
2959+      if (i == keys.length) {
2960+        var mapKeys = [];
2961+        for (var name in mapDict)
2962+          mapKeys.push(name);
2963+        mapKeys.sort();
2964+        finished({dict: mapDict, keys: mapKeys});
2965+      } else
2966+        progress("map", i / keysLength, continueMap);
2967+    }
2968+
2969+    continueMap();
2970+  },
2971+
2972+  /**
2973+   *  da.db.SingleThreadedMapReducer.reduce(reduce, mapResult, progress, chunkSize, finished) -> undefined
2974+   *  - reduce (Function): reduce function.
2975+   *  - mapResult (Object): Object returned by [[da.db.SingleThreadedMapReducer.map]].
2976+   *  - progress (Function): progress reportiong function. Called with `"reduce"` as first argument.
2977+   *  - chunkSize (Number): number of documents to process at once.
2978+   *  - finished (Function): called when reduce process finishes.
2979+   *  - rereduce (Boolean | Object): object which will be passed to `reduce` during the rereduce process.
2980+   *
2981+   *  Please refer to [CouchDB's docs on map and reduce functions](http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views#Basics)
2982+   *  for more detailed usage details.
2983+   **/
2984+  reduce: function STMR_reduce(reduce, mapResult, progress,
2985+                               chunkSize, finished, rereduce) {
2986+    var rows = [],
2987+        mapDict = mapResult.dict,
2988+        mapKeys = mapResult.keys,
2989+        i = 0;
2990+    rereduce = rereduce || {};
2991+
2992+    function continueReduce() {
2993+      var iAtStart = i;
2994+
2995+      do {
2996+        var key   = mapKeys[i],
2997+            item  = mapDict[key]
2998+       
2999+        rows.push({
3000+          key: key,
3001+          value: reduce(key, item.values, false)
3002+        });
3003+       
3004+        i++;
3005+      } while (i - iAtStart < chunkSize &&
3006+               i < mapKeys.length)
3007+
3008+      if (i == mapKeys.length) {
3009+        finished(rows);
3010+      } else
3011+        progress("reduce", i / mapKeys.length, continueReduce);
3012+    }
3013+
3014+    continueReduce();
3015+  }
3016+};
3017+
3018+da.db.BrowserCouch = BrowserCouch;
3019+da.db.BrowserCouch.Dictionary = Dictionary;
3020+da.db.SingleThreadedMapReducer = SingleThreadedMapReducer;
3021+da.db.WebWorkerMapReducer = WebWorkerMapReducer;
3022+
3023+})();
3024addfile ./contrib/musicplayer/src/libs/db/DocumentTemplate.js
3025hunk ./contrib/musicplayer/src/libs/db/DocumentTemplate.js 1
3026+//#require "libs/db/db.js"
3027+//#require "libs/db/BrowserCouch.js"
3028+//#require "libs/vendor/Math.uuid.js"
3029+//#require "libs/util/util.js"
3030+
3031+(function () {
3032+/** section: Database
3033+ *  class da.db.DocumentTemplate
3034+ *  implements Events
3035+ * 
3036+ *  Abstract class for manufacturing document templates. (ie. Model from MVC)
3037+ **/
3038+var DocumentTemplate = new Class({
3039+  Implements: Events,
3040
3041+  /**
3042+   *  da.db.DocumentTemplate#belongsTo -> Object
3043+   * 
3044+   *  Provides belongs-to-many relationsip found in may ORM libraries.
3045+   * 
3046+   *  #### Example
3047+   *      da.db.DocumentTemplate.registerType("Artist", new Class({
3048+   *        Extends: da.db.DocumentTemplate
3049+   *      }));
3050+   *
3051+   *      var queen = new da.db.DocumentTemplate.Artist({
3052+   *        id: 0,
3053+   *        title: "Queen"
3054+   *      });
3055+   *     
3056+   *      da.db.DocumentTemplate.registerType("Song", new Class({
3057+   *        Extends: da.db.DocumentTemplate,
3058+   *        belongsTo: {
3059+   *          artist: "Artist" // -> artist_id property will be used to create a new Artist
3060+   *        }
3061+   *      }));
3062+   *     
3063+   *      var yeah = new da.db.DocumentTemplate.Song({
3064+   *        artist_id: queen.id,
3065+   *        album_id: 5,
3066+   *        title: "Yeah"
3067+   *      });
3068+   *
3069+   *      yeah.get("artist", function (artist) {
3070+   *        console.log("Yeah by " + artist.get("title"));
3071+   *      });
3072+   * 
3073+  **/
3074+  belongsTo: {},
3075
3076+  /**
3077+   *  da.db.DocumentTemplate#hasMany -> Object
3078+   *
3079+   *  Provides has-many relationship between database documents.
3080+   *
3081+   *  #### Example
3082+   *  If we defined `da.db.DocumentTemplate.Artist` in [[da.db.DocumentTemplate#belongsTo]] like:
3083+   *
3084+   *      da.db.DocumentTemplate.registerType("Artist", new Class({
3085+   *        Extends: da.db.DocumentTemplate,
3086+   *        hasMany: {
3087+   *          songs: ["Song", "artist_id"]
3088+   *        }
3089+   *      }));
3090+   *
3091+   *  And assumed that `"artist_id"` is the name of the property which holds id of an `Artist`,
3092+   *  while `"Song"` represents the type of the document.
3093+   *
3094+   *  Then we can obtain all the songs by given a artist with:
3095+   *
3096+   *      queen.get("songs", function (songs) {
3097+   *        console.log("Queen songs:")
3098+   *        for(var n = 0, m = songs.length; n < m; n++)
3099+   *          console.log(songs[n].get("title"));
3100+   *      });
3101+   **/
3102+  hasMany: {},
3103
3104+  /**
3105+   *  new da.db.DocumentTemplate(properties[, events])
3106+   *  - properties (Object): document's properties.
3107+   *  - events (Object): default events.
3108+   **/
3109+  initialize: function (properties, events) {
3110+    this.doc = properties;
3111+    if(!this.doc.id)
3112+      this.doc.id = Math.uuid();
3113+
3114+    this.id = this.doc.id;
3115+    this.doc.type = this.constructor.type;
3116+    if(!this.constructor.db)
3117+      this.constructor.db = function () {
3118+        return Application.db
3119+      };
3120+
3121+    // Time delay is set so class can finish initialization
3122+    this.addEvents(events);
3123+    this.fireEvent("create", [this], 1);
3124+  },
3125
3126+  /**
3127+   *  da.db.DocumentTemplate#id -> "id of the document"
3128+   * 
3129+   *  Shortcut for [[da.db.DocumentTemplate#get]]`("id")`.
3130+   **/
3131+  id: null,
3132
3133+  /**
3134+   *  da.db.DocumentTemplate#get(key[, callback]) -> Object | false | this
3135+   *  - key (String): name of the property.
3136+   *  - callback (Function): needed only if `key` points to an property defined by an relationship.
3137+   **/
3138+  get: function (key, callback) {
3139+    if(key in this.doc)
3140+      return this.doc[key];
3141+   
3142+    if(!callback)
3143+      return false;
3144+   
3145+    if(key in this.belongsTo) {
3146+      var cache_key = "_belongs_to_" + key,
3147+          cached = this[cache_key];
3148+   
3149+      if(cached && cached.id === this.doc[key + "_id"])
3150+        return callback(cached);
3151+     
3152+      if(!this.doc[key + "_id"])
3153+        return callback(null);
3154+     
3155+      DocumentTemplate.find({
3156+        properties: {
3157+          id:   this.doc[key + "_id"],
3158+          type: this.belongsTo[key]
3159+        },
3160+       
3161+        onSuccess: function (doc) {
3162+          this[cache_key] = doc;
3163+          callback(doc);
3164+        }.bind(this),
3165+       
3166+        onFailure: callback
3167+      }, this.constructor.db());
3168+    } else if(key in this.hasMany) {
3169+      var relation = this.hasMany[key],
3170+          props = {type: relation[0]};
3171+     
3172+      props[relation[1]] = this.id;
3173+     
3174+      DocumentTemplate.find({
3175+        properties: props,
3176+        onSuccess: callback,
3177+        onFailure: callback
3178+      }, DocumentTemplate[relation[0]].db());
3179+    }
3180+   
3181+    return this;
3182+  },
3183
3184+  /**
3185+   *  da.db.DocumentTemplate#set(properties) -> this
3186+   *  da.db.DocumentTemplate#set(key, value) -> this
3187+   *  - properties (Object): updated properties.
3188+   *  fires propertyChange
3189+   **/
3190+  set: function (properties) {
3191+    if(arguments.length == 2) {
3192+      var key = properties;
3193+      properties = {};
3194+      properties[key] = arguments[1];
3195+    }
3196+   
3197+    $extend(this.doc, properties);
3198+    this.fireEvent("propertyChange", [properties, this]);
3199+   
3200+    return this;
3201+  },
3202
3203+  /**
3204+   *  da.db.DocumentTemplate#remove(property) -> this
3205+   *  - property (String): property to be removed.
3206+   *  fires propertyRemove
3207+   **/
3208+  remove: function (property) {
3209+    if(property !== "_id")
3210+      delete this.doc[property];
3211+   
3212+    this.fireEvent("propertyRemove", [property, this]);
3213+    return this;
3214+  },
3215
3216+  /**
3217+   *  da.db.DocumentTemplate#save([callback]) -> this
3218+   *  - callback (Function): function called after `save` event.
3219+   *  fires save
3220+   **/
3221+  save: function (callback) {
3222+    this.constructor.db().put(this.doc, function () {
3223+      this.fireEvent("save", [this]);
3224+      if(callback)
3225+        callback(this);
3226+    }.bind(this));
3227+   
3228+    return this;
3229+  },
3230
3231+  /**
3232+   *  da.db.DocumentTemplate#update(properties[, cb]) -> this
3233+   *  - properties (Object): new properties.
3234+   *  - callback (Function): called after `save`.
3235+   * 
3236+   *  Calls [[da.db.DocumentTemplate#set]] and [[da.db.DocumentTemplate#save]].
3237+   **/
3238+  update: function (properties, cb) {
3239+    this.set(properties);
3240+    this.save(cb);
3241+    return this;
3242+  },
3243
3244+  /**
3245+   *  da.db.DocumentTemplate#destroy([callback]) -> this
3246+   *  - callback (Function): function called after `destroy` event.
3247+   * 
3248+   *  Removes all document's properties except for `id` and adds `_deleted` property.
3249+   **/
3250+  destroy: function (callback) {
3251+    this.doc = {id: this.id, _deleted: true};
3252+    this.constructor.db().put(this.doc, function () {
3253+      this.fireEvent("destroy", [this]);
3254+      if(callback)
3255+        callback(this);
3256+    });
3257+   
3258+    return this;
3259+  }
3260+});
3261+
3262+DocumentTemplate.extend({
3263+  /**
3264+   *  da.db.DocumentTemplate.find(options[, db]) -> undefined
3265+   *  - options.properties (String | Object | Function): properties document must have or an function which checks document's properties.
3266+   *    If `String` is provided, it's assumed that it represents document's `id`.
3267+   *  - options.onSuccess (Function): function called once document is found.
3268+   *  - options.onFailure (Function): function called if no documents are found.
3269+   *  - options.onlyFirst (Bool): gives back only first result.
3270+   *  - db (BrowserCouch): if not provided, `Application.db` is used.
3271+   **/
3272+  find: function (options, db) {
3273+    if(!options.onSuccess)
3274+      return false;
3275+    if(!options.onFailure)
3276+      options.onFailure = $empty;
3277+    if(typeof options.properties === "string")
3278+      options.properties = {id: options.properties}
3279+   
3280+    var map_fn, props = options.properties;
3281+    if(typeof properties === "function")
3282+      map_fn = function (doc, emit) {
3283+        if(doc && !doc._deleted && props(doc))
3284+          emit(doc.id, doc);
3285+        };
3286+    else
3287+      map_fn = function (doc, emit) {
3288+        if(doc && !doc._deleted && Hash.containsAll(doc, props))
3289+          emit(doc.id, doc);
3290+      };
3291+   
3292+    (db || da.db.DEFAULT).view({
3293+      temporary: true,
3294+      map: map_fn,
3295+      finished: function (result) {
3296+        if(!result.rows.length)
3297+          return options.onFailure();
3298+       
3299+        var n = result.rows.length;
3300+        while(n--) {
3301+          var row = result.rows[n].value,
3302+              type = DocumentTemplate[row.type];
3303+           
3304+          result.rows[n] = type ? new type(row) : row;
3305+        }
3306+       
3307+        options.onSuccess(options.onlyFirst ? result.rows[0] : result.rows);
3308+      }
3309+    });
3310+  },
3311
3312+  /**
3313+   *  da.db.DocumentTemplate.findFirst(options[, db]) -> undefined
3314+   *  - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here.
3315+   **/
3316+  findFirst: function (options, db) {
3317+    options.onlyFirst = true;
3318+    this.find(options, db);
3319+  },
3320
3321+  /**
3322+   *  da.db.DocumentTemplate.findOrCreate(options[, db]) -> undefined
3323+   *  - options (Object): same options as in [[da.db.DocumentTemplate.find]] apply here.
3324+   *  - options.properties.type (String): must be set to the desired [[da.db.DocumentTemplate]] type.
3325+   **/
3326+  findOrCreate: function (options, db) {
3327+    options.onSuccess = options.onSuccess || $empty;
3328+    options.onFailure = function () {
3329+      options.onSuccess(new DocumentTemplate[options.properties.type](options.properties), true);
3330+    };
3331+    this.findFirst(options, db);
3332+  },
3333
3334+  /**
3335+   *  da.db.DocumentTemplate.registerType(typeName[, db = Application.db], template) -> da.db.DocumentTemplate
3336+   *  - typeName (String): name of the type. ex.: `Car`, `Chocolate` etc.
3337+   *  - db (BrowserCouch): database to be used.
3338+   *  - template (da.db.DocumentTemplate): the actual [[da.db.DocumentTemplate]] [[Class]].
3339+   *
3340+   *  New classes are accessible from `da.db.DocumentTemplate.<typeName>`.
3341+   **/
3342+  registerType: function (type, db, template) {
3343+    if(arguments.length === 2) {
3344+      template = db;
3345+      db = null;
3346+    }
3347+   
3348+    template.type = type;
3349+    // This is a function so we can
3350+    // return a reference to the original instance
3351+    // of DB, otherwise, due to MooTools' inheritance
3352+    // we would get a new copy.
3353+    if(db)
3354+      template.db = function () { return db };
3355+    else
3356+      template.db = function () { return da.db.DEFAULT };
3357+   
3358+    template.find = function (options) {
3359+      options.properties.type = type;
3360+      DocumentTemplate.find(options, db);
3361+    };
3362+   
3363+    template.findFirst = function (options) {
3364+      options.properties.type = type;
3365+      DocumentTemplate.findFirst(options, db);
3366+    };
3367+   
3368+    template.create = function (properties, callback) {
3369+      return (new template(properties)).save(callback);
3370+    };
3371+
3372+    template.findOrCreate = function (options) {
3373+      options.properties.type = type;
3374+      DocumentTemplate.findOrCreate(options, db);
3375+    };
3376+   
3377+    template.db().view({
3378+      id: type,
3379+      map: function (doc, emit) {
3380+        if(doc && doc.type === type)
3381+          emit(doc.id, doc);
3382+      },
3383+      finished: $empty
3384+    });
3385+   
3386+    DocumentTemplate[type] = template;
3387+    return template;
3388+  }
3389+});
3390+
3391+da.db.DocumentTemplate = DocumentTemplate;
3392+
3393+})();
3394addfile ./contrib/musicplayer/src/libs/db/PersistStorage.js
3395hunk ./contrib/musicplayer/src/libs/db/PersistStorage.js 1
3396+//#require "libs/vendor/persist-js/src/persist.js"
3397+//#require "libs/db/db.js"
3398+
3399+(function () {
3400+/** section: Database
3401+ *  class da.db.PersistStorage
3402+ * 
3403+ *  Interface between PersistJS and BrowserCouch.
3404+ **/
3405+/*
3406+ *  new da.db.PersistStorage(database_name)
3407+ *  - database_name (String): name of the database.
3408+ **/
3409+function PersistStorage (db_name) {
3410+  var storage = new Persist.Store(db_name || "tahoemp");
3411
3412+  /**
3413+   *  da.db.PersistStorage#get(key, callback) -> undefined
3414+   *  - key (String): name of the property
3415+   *  - callback (Function): will be called once data is fetched,
3416+   *    which will be passed as first argument.
3417+   **/
3418+  this.get = function (key, cb) {
3419+    storage.get(key, function (ok, value) {
3420+      cb(value ? JSON.parse(value) : null, ok);
3421+    });
3422+  };
3423
3424+  /**
3425+   *  da.db.PersistStorage#put(key, value[, callback]) -> undefined
3426+   *  - key (String): name of the property.
3427+   *  - value (Object): value of the property.
3428+   *  - callback (Function): will be called once data is saved.
3429+   **/
3430+  this.put = function (key, value, cb) {
3431+    storage.set(key, JSON.stringify(value));
3432+    if(cb) cb();
3433+  };
3434
3435+  return this;
3436+}
3437+
3438+da.db.PersistStorage = PersistStorage;
3439+})();
3440addfile ./contrib/musicplayer/src/libs/db/db.js
3441hunk ./contrib/musicplayer/src/libs/db/db.js 1
3442+/**
3443+ *  == Database ==
3444+ * 
3445+ *  Map/Reduce, storage and model APIs.
3446+ **/
3447+
3448+/** section: Database
3449+ * da.db
3450+ **/
3451+if(typeof da.db === "undefined")
3452+  da.db = {};
3453adddir ./contrib/musicplayer/src/libs/ui
3454addfile ./contrib/musicplayer/src/libs/ui/Column.js
3455hunk ./contrib/musicplayer/src/libs/ui/Column.js 1
3456+//#require "libs/ui/ui.js"
3457+
3458+/** section: UserInterface
3459+ *  class da.ui.Column
3460+ *  implements Events, Options
3461+ * 
3462+ *  Widget which can efficiently display large amounts of items in a list.
3463+ **/
3464+da.ui.Column = new Class({
3465+  Implements: [Events, Options],
3466
3467+  options: {
3468+    id:             undefined,
3469+    rowHeight:     30,
3470+    totalCount:     0,
3471+    renderTimeout: 120,
3472+    itemClassNames: "column_item"
3473+  },
3474+  /**
3475+   *  new da.ui.Column(options)
3476+   *  - options.id (String): desired ID of the column's DIV element, `_column` will be appended.
3477+   *    if ommited, random one will be generated.
3478+   *  - options.rowHeight (Number): height of an row. Defaults to 30.
3479+   *  - options.totalCount (Number): number of items this column has to show in total.
3480+   *  - options.itemClassNames (String): CSS class names added to each item. Defaults to `column_item`.
3481+   *  - options.renderTimeout (Number): milliseconds to wait during the scroll before rendering
3482+   *    items. Defaults to 120.
3483+   * 
3484+   *  Creates a new Column.
3485+   * 
3486+   *  ##### Notes
3487+   *  When resizing (height) of the column use [[Element#set]] function provided by MooTools
3488+   *  which properly fires `resize` event.
3489+   *     
3490+   *      column._el.set("height", window.getHeight());
3491+   * 
3492+   **/
3493+  initialize: function (options) {
3494+    this.setOptions(options);
3495+    if(!this.options.id)
3496+      this.options.id = "column_" + Math.uuid(5);
3497+
3498+    this._populated = false;
3499+    // #_rendered will contain keys of items which have been rendered.
3500+    // What is a key is up to particular implementation.
3501+    this._rendered = [];
3502+   
3503+    this._el = new Element("div", {
3504+      id: options.id,
3505+      'class': 'column',
3506+      styles: {
3507+        overflowX: "hidden",
3508+        overflowY: "auto",
3509+        position: "relative"
3510+      }
3511+    });
3512+   
3513+    // weight is used to force the browser
3514+    // to show scrollbar with right proportions.
3515+    this._weight = new Element("div", {
3516+      styles: {
3517+        position: "absolute",
3518+        top:    0,
3519+        left:   0,
3520+        width:  1,
3521+        height: 1
3522+      }
3523+    });
3524+    this._weight.injectBottom(this._el);
3525+
3526+    // scroll event is fired for even smallest changes
3527+    // of scrollbars positions, since rendering items can be
3528+    // expensive a small timeout will be set in order to save
3529+    // some bandwidth - the negative side is that flicker is seen
3530+    // while scrolling.
3531+    var scroll_timer = null,
3532+        timeout = this.options.renderTimeout,
3533+        timeout_fn = this.render.bind(this);
3534+
3535+    this._el.addEvent("scroll", function () {
3536+      clearTimeout(scroll_timer);
3537+      scroll_timer = setTimeout(scroll_timer, timeout);
3538+    });
3539+   
3540+    // We're caching lists' height so we won't have to
3541+    // ask for it in every #render() - which can be quite expensiv.
3542+    this._el.addEvent("resize", function () {
3543+      this._el_height = this._el.getHeight();
3544+    }.bind(this));
3545+  },
3546
3547+  /**
3548+   *  da.ui.Column#render() -> this | false
3549+   * 
3550+   *  Renders all of items which are in current viewport in a batch.
3551+   * 
3552+   *  Returns `false` if all of items have already been rendered.
3553+   * 
3554+   *  Items are rendered in groups of (`div` tags with `column_items_box` CSS class).
3555+   *  The number of items is determined by number of items which can fit in viewport + five
3556+   *  items before and 10 items after current viewport.
3557+   *  Each item has CSS classes defined in `options.itemClassNames` and have a `column_index`
3558+   *  property stored.
3559+   **/
3560+  render: function () {
3561+    if(!this._populated)
3562+      this.populate();
3563+    if(this._rendered.length === this.options.totalCount + 1)
3564+      return false;
3565+   
3566+    // We're pre-fetching previous 5 and next 10 items
3567+    // which are outside of current viewport
3568+    var total_count = this.options.totalCount,
3569+        ids = this.getVisibleIndexes(),
3570+        n = Math.max(0, ids[0] - 6),
3571+        m = Math.max(Math.min(ids[1] + 10, total_count), total_count),
3572+        box = new Element("div", {"class": "column_items_box"}),
3573+        item_class = this.options.itemClassNames,
3574+        first_rendered = null;
3575+
3576+    for( ; n < m; n++) {
3577+      if(!this._rendered.contains(n)) {
3578+        // First item in viewport could be already rendered
3579+        // this helps minimizing amount of DOM nodes that will be inserted
3580+        if(first_rendered === null)
3581+          first_rendered = n;
3582+
3583+        this.renderItem(n).addClass(item_class).store("column_index", n).injectBottom(box);
3584+        this._rendered.push(n);
3585+      }
3586+    }
3587+   
3588+    if(first_rendered !== null) {
3589+      var coords = this.getBoxCoords(first_rendered);
3590+      box.setStyles({
3591+        position: "absolute",       
3592+        top: coords[0],
3593+        left: coords[1]
3594+      }).injectBottom(this._el);
3595+    }
3596+   
3597+    return this;
3598+  },
3599
3600+  /**
3601+   *  da.ui.Column#populate() -> this
3602+   *  fires resize
3603+   * 
3604+   *  Positiones weight element and fires `resize` event. This method should ignore `_populated` property.
3605+   **/
3606+  populate: function () {
3607+    var o = this.options;
3608+    this._populated = true;
3609+    this._weight.setStyle("top", o.rowHeight * o.totalCount /*+ o.rowHeight*/);
3610+    this._el.fireEvent("resize");
3611+   
3612+    return this;
3613+  },
3614
3615+  /**
3616+   *  da.ui.Column#rerender() -> this
3617+   **/
3618+  rerender: function () {
3619+    var weight = this._weight;
3620+    this._el.empty();
3621+    this._el.grab(weight);
3622+   
3623+    this._rendered = [];
3624+    this._populated = false;
3625+    return this.render();
3626+  },
3627+  /**
3628+   *  da.ui.Column#updateTotalCount(totalCount) -> this | false
3629+   *  - totalCount (Number): total number of items this column is going to display
3630+   *
3631+   *  Provides means to update `totalCount` option after column has already been rendered/initialized.
3632+   **/
3633+  updateTotalCount: function (total_count) {
3634+    this.options.totalCount = total_count;
3635+    return this.populate();
3636+  },
3637
3638+  /**
3639+   *  da.ui.Column#renderItem(index) -> Element
3640+   *  - index (Object): could be a String or Number, internal representation of data.
3641+   * 
3642+   *  Constructs and returns new Element without adding it to the `document`.
3643+   **/
3644+  renderItem: function(index) {
3645+    console.warn("Column.renderItem(index) should be overwritten", this);
3646+    return new Element("div", {html: index});
3647+  },
3648
3649+  /**
3650+   *  da.ui.Column#getBoxCoords(index) -> [Number, Number]
3651+   *  - index (Number): index of the first item in a box.
3652+   * 
3653+   *  Returns X and Y coordinates at which item with given `index` should be rendered at.
3654+   **/
3655+  getBoxCoords: function(index) {
3656+    return [this.options.rowHeight * index, 0];
3657+  },
3658+
3659+  /**
3660+   *  da.ui.Column#getVisibleIndexes() -> Array
3661+   * 
3662+   *  Returns an array with indexes of first and last item in visible portion of list.
3663+   **/
3664+  getVisibleIndexes: function () {
3665+    // Math.round() and Math.ceil() are used in such combination
3666+    // to include items which could be only partially in viewport
3667+    var rh = this.options.rowHeight,
3668+        per_viewport = Math.round(this._el_height / rh),
3669+        first = Math.ceil(this._el.getScroll().y / rh);
3670+    if(first > 0) first--;
3671+   
3672+    return [first, first + per_viewport];
3673+  },
3674+
3675+  /**
3676+   *  da.ui.Column#injectBottom(element) -> this
3677+   *  - element (Element): element to which column should be appended.
3678+   * 
3679+   *  Injects column at the bottom of provided element.
3680+  **/
3681+  injectBottom: function(el) {
3682+    this._el.injectBottom(el);
3683+    return this;
3684+  },
3685
3686+  /**
3687+   *  da.ui.Column#destory() -> this
3688+   * 
3689+   *  Removes column from DOM.
3690+   **/
3691+  destroy: function (dispose) {
3692+    this._el.destroy();
3693+    delete this._el;
3694+    return this;
3695+  },
3696
3697+  /**
3698+   *  da.ui.Column#toElement() -> Element
3699+   **/
3700+  toElement: function () {
3701+    return this._el;
3702+  }
3703+});
3704addfile ./contrib/musicplayer/src/libs/ui/Dialog.js
3705hunk ./contrib/musicplayer/src/libs/ui/Dialog.js 1
3706+//#require "libs/ui/ui.js"
3707+
3708+/** section: UserInterface
3709+ *  class da.ui.Dialog
3710+ *
3711+ *  Class for working with interface dialogs.
3712+ **/
3713+da.ui.Dialog = new Class({
3714+  Implements: [Events, Options],
3715+
3716+  options: {
3717+    title: null,
3718+    hideOnOutsideClick: true,
3719+    show: false
3720+  },
3721+
3722+  /**
3723+   *  new da.ui.Dialog(options)
3724+   *  - options.title (String): title of the dialog. optional.
3725+   *  - options.hideOnOutsideClick (Boolean): if `true`, the dialog will be hidden when
3726+   *    click outside the dialog element (ie. on the dimmed portion of screen) occurs.
3727+   *  - options.show (Boolean): if `true` the dialog will be shown immediately as it's created.
3728+   *    Defaults to `false`.
3729+   *  - options.html (Element): contents of the.
3730+   *
3731+   *  To the `options.html` element `dialog` CSS class name will be added and
3732+   *  the element will be wrapped into a `div` with `dialog_wrapper` CSS class name.
3733+   *
3734+   *  If `options.title` is provided, the title element will be injected at the top of
3735+   *  `options.html` and will be given `dialog_title` CSS class name.
3736+   *
3737+   *  #### Notes
3738+   *  All dialogs are hidden by default, use [[Dialog.show]] to show them immediately
3739+   *  after they are created method.
3740+   *
3741+   *  #### Example
3742+   *      new da.ui.Dialog({
3743+   *        title: "What's your name?"
3744+   *        html: new Element("div", {
3745+   *          html: "Hello!"
3746+   *        }),
3747+   *        show: true
3748+   *      });
3749+   *
3750+   **/
3751+  initialize: function (options) {
3752+    this.setOptions(options);
3753+    if(!this.options.html)
3754+     throw "options.html must be provided when creating an Dialog";
3755+
3756+    this._el = new Element("div", {
3757+      "class": "dialog_wrapper"
3758+    });
3759+    if(!this.options.show)
3760+      this._el.style.display = "none";
3761+   
3762+    if(this.options.title)
3763+      if(typeof this.options.title === "string")
3764+        (new Element("h2", {
3765+          html: this.options.title,
3766+          "class": "dialog_title no_selection"
3767+        })).inject(this.options.html, "top");
3768+      else if($type(this.options.title) === "element")
3769+        this.options.title.inject(this.options.html, "top");
3770+   
3771+    if(this.options.hideOnOutsideClick)
3772+      this._el.addEvent("click", this.hide.bind(this));
3773+   
3774+    this._el.grab(options.html.addClass("dialog"));
3775+    document.body.grab(this._el);
3776+  },
3777+
3778+  /**
3779+   *  da.ui.Dialog#show() -> this
3780+   *  fires show
3781+   **/
3782+  show: function () {
3783+    this._el.show();
3784+    this.fireEvent("show", [this]);
3785+    return this;
3786+  },
3787+
3788+  /**
3789+   *  da.ui.Dialog#hide(event) -> this
3790+   *  fires hide
3791+   **/
3792+  hide: function (event) {
3793+    if(event && event.target !== this._el)
3794+      return this;
3795+
3796+    this._el.hide();
3797+    this.fireEvent("hide", [this]);
3798+    return this;
3799+  },
3800+
3801+  /**
3802+   *  da.ui.Dialog#destroy() -> this
3803+   **/
3804+  destory: function () {
3805+    this.options.html.destroy();
3806+    this._el.destroy();
3807+    return this;
3808+  }
3809+});
3810+
3811addfile ./contrib/musicplayer/src/libs/ui/Menu.js
3812hunk ./contrib/musicplayer/src/libs/ui/Menu.js 1
3813+//#require "libs/ui/ui.js"
3814+
3815+(function () {
3816+var VISIBLE_MENU;
3817+
3818+/** section: UserInterface
3819+ *  class da.ui.Menu
3820+ *  implements Events, Options
3821+ * 
3822+ *  Lightweight menu class.
3823+ * 
3824+ *  #### Example
3825+ * 
3826+ *      var file_menu = new da.ui.Menu({
3827+ *        items: {
3828+ *          neu:      {html: "New", href: "#"},
3829+ *          neu_tpl:  {html: "New from template", href: "#"},
3830+ *          open:     {html: "Open", href: "#"},
3831+ *         
3832+ *          _sep1:     da.ui.Menu.separator,
3833+ *         
3834+ *          close:    {html: "Close", href: "#"},
3835+ *          save:     {html: "Save", href: "#"},
3836+ *          save_all: {html: "Save all", href: "#", "class": "disabled"},
3837+ *         
3838+ *          _sep2:    da.ui.Menu.separator,
3839+ *         
3840+ *          quit:     {html: "Quit", href: "#", onClick: function () {
3841+ *            confirm("Are you sure?")
3842+ *          }}
3843+ *        },
3844+ *       
3845+ *        position: {
3846+ *          position: "topLeft"
3847+ *        },
3848+ *       
3849+ *        onClick: function (key, event, element) {
3850+ *          console.log("knock knock", key);
3851+ *        }
3852+ *      });
3853+ *     
3854+ *      file_menu.show();
3855+ * 
3856+ *  Values of properties in `items` are actually second arguments for MooTools'
3857+ *  `new Element()` and therefore provide great customization ability.
3858+ * 
3859+ *  `position` property will be passed to MooTools' `Element.position()` method,
3860+ *  and defaults to `bottomRight`.
3861+ *
3862+ *  #### Events
3863+ *  - `click` - arguments: key of the clicked item, clicked element
3864+ *  - `show`
3865+ *  - `hide`
3866+ * 
3867+ *  #### Notes
3868+ *  `href` attribute is added to all items in order to enable
3869+ *  keyboard navigation with tab key.
3870+ * 
3871+ *  #### See also
3872+ *  * [MooTools Element class](http://mootools.net/docs/core/Element/Element#Element:constructor)
3873+ **/
3874+
3875+da.ui.Menu = new Class({
3876+  Implements: [Events, Options],
3877
3878+  options: {
3879+    items: {},
3880+    position: {
3881+      position: "bottomLeft"
3882+    }
3883+  },
3884
3885+  /**
3886+   *  da.ui.Menu#last_clicked -> Element
3887+   * 
3888+   *  Last clicked menu item.
3889+   **/
3890+  last_clicked: null,
3891
3892+  /**
3893+   *  new da.ui.Menu([options = {}])
3894+   *  - options.items (Object): menu items.
3895+   *  - options.position (Object): menu positioning parameters.
3896+   **/
3897+  initialize: function (options) {
3898+    this.setOptions(options);
3899+   
3900+    this._el = (new Element("ul")).addClass("menu").addClass("no_selection");
3901+    this._el.style.display = "none";
3902+    this._el.addEvent("click:relay(.menu_item a)", this.click.bind(this));
3903+    //this._el.injectBottom(document.body);
3904+   
3905+    this.render();
3906+  },
3907
3908+  /**
3909+   *  da.ui.Menu#render() -> this
3910+   * 
3911+   *  Renders the menu items and adds them to the document.
3912+   *  Menu element is an `ul` tag appeded to the bottom of `document.body` and has `menu` CSS class.
3913+   **/
3914+  render: function () {
3915+    var items = this.options.items;
3916+    this._el.dispose().empty();
3917+   
3918+    for(var id in items)
3919+      this._el.grab(this.renderItem(id));
3920+   
3921+    document.body.grab(this._el);
3922+    return this;
3923+  },
3924
3925+  /**
3926+   *  da.ui.Menu#renderItem(id) -> Element
3927+   *  - id (String): id of the menu item.
3928+   * 
3929+   *  Renders item without attaching it to DOM.
3930+   *  Item is a `li` tag with `menu_item` CSS class. `li` tag contains an `a` tag with the item's text.
3931+   *  Each `li` tag also has a `menu_key` property set, which can be retrived with:
3932+   *       
3933+   *        menu.toElement().getItems('.menu_item').retrieve("menu_key")
3934+   * 
3935+   *  If the item was defined with function than those tag names might not be used,
3936+   *  but CSS class names are guaranteed to be there in both cases.
3937+   **/
3938+  renderItem: function (id) {
3939+    var options = this.options.items[id], el;
3940+   
3941+    if(typeof options === "function")
3942+      el = options(this).addClass("menu_item");
3943+    else
3944+      el = new Element("li").grab(new Element("a", options));
3945+     
3946+    return el.addClass("menu_item").store("menu_key", id);
3947+  },
3948
3949+  /**
3950+   *  da.ui.Menu#addItems(items) -> this
3951+   *  - items (Object): key-value pairs of items to be added to the menu.
3952+   * 
3953+   *  Adds items to the bottom of menu and renders them.
3954+   **/
3955+  addItems: function (items) {
3956+    $extend(this.options.items, items);
3957+    return this.render();
3958+  },
3959
3960+  /**
3961+   *  da.ui.Menu#addItem(id, value) -> this
3962+   *  - id (String): id of the item.
3963+   *  - value (Object | Function): options for [[Element]] class or function which will render the item.
3964+   * 
3965+   *  If `value` is an [[Object]] then it will be passed as second argument to MooTools's [[Element]] class.
3966+   *  If `value` is an [[Function]] then it has return an [[Element]],
3967+   *  first argument of the function is id of the item that needs to be rendered.
3968+   **/
3969+  addItem: function (id, value) {
3970+    this.options.items[id] = value;
3971+    this._el.grab(this.renderItem(id));
3972+    return this;
3973+  },
3974
3975+  /**
3976+   *  da.ui.Menu#removeItem(id) -> this
3977+   *  - id (String): id of the item.
3978+   * 
3979+   *  Removes an item from the menu.
3980+   **/
3981+  removeItem: function (id) {
3982+    delete this.options.items[id];
3983+    return this.render();
3984+  },
3985
3986+  /**
3987+   *  da.ui.Menu#addSeparator() -> this
3988+   * 
3989+   *  Adds separator to the menu.
3990+   **/
3991+  addSeparator: function () {
3992+    return this.addItem("separator_" + Math.uuid(3), da.ui.Menu.separator);
3993+  },
3994
3995+  /**
3996+   *  da.ui.Menu#click(event, element) -> this
3997+   *  - event (Event): DOM event or `null`.
3998+   *  - element (Element): list item which was clicked.
3999+   *  fires: click
4000+   **/
4001+  click: function (event, element) {
4002+    this.hide();
4003+   
4004+    if(!element.className.contains("menu_item"))
4005+      element = element.getParent(".menu_item");
4006+    if(!element)
4007+      return this;
4008+   
4009+    this.fireEvent("click", [element.retrieve("menu_key"), event, element]);
4010+    this.last_clicked = element;
4011+   
4012+    return this;
4013+  },
4014
4015+  /**
4016+   *  da.ui.Menu#show([event]) -> this
4017+   *  - event (Event): click or some other DOM event with coordinates.
4018+   *  fires show
4019+   * 
4020+   *  Shows the menu. If event is present than menus location will be adjusted according to
4021+   *  event's coordinates and position option.
4022+   *  In case the menu is already visible, it will be hidden.
4023+   **/
4024+  show: function (event) {
4025+    if(VISIBLE_MENU) {
4026+      if(VISIBLE_MENU == this)
4027+        return this.hide();
4028+      else
4029+        VISIBLE_MENU.hide();
4030+    }
4031+
4032+    VISIBLE_MENU = this;
4033+   
4034+    if(event)
4035+      event.stop();
4036+   
4037+    if(event && event.target)
4038+      this._el.position($extend({
4039+        relativeTo: event.target
4040+      }, this.options.position));
4041+   
4042+    this._el.style.zIndex = 5;
4043+    this._el.style.display = "block";
4044+    this._el.focus();
4045+   
4046+    this.fireEvent("show");
4047+   
4048+    return this;
4049+  },
4050
4051+  /**
4052+   *  da.ui.Menu#hide() -> this
4053+   *  fires hide
4054+   * 
4055+   *  Hides the menu.
4056+   **/
4057+  hide: function () {
4058+    if(this._el.style.display === "none")
4059+      return this;
4060+   
4061+    VISIBLE_MENU = null;
4062+    this._el.style.display = "none";
4063+    this.fireEvent("hide");
4064+   
4065+    return this;
4066+  },
4067
4068+  /**
4069+   *  da.ui.Menu#destroy() -> this
4070+   * 
4071+   *  Destroys the menu.
4072+   **/
4073+  destroy: function () {
4074+    this._el.destroy();
4075+    delete this._el;
4076+    return this;
4077+  },
4078
4079+  /**
4080+   *  da.ui.Menu#toElement() -> Element
4081+   * 
4082+   *  Returns menu element.
4083+   **/
4084+  toElement: function () {
4085+    return this._el;
4086+  }
4087+});
4088+
4089+/**
4090+ *  da.ui.Menu.separator -> Object
4091+ * 
4092+ *  Use this object as a separator.
4093+ **/
4094+da.ui.Menu.separator = {
4095+  "class": "menu_separator",
4096+  html: "<hr/>",
4097+  onClick: function (event) {
4098+    if(event)
4099+      event.stop();
4100+  }
4101+};
4102+
4103+// Hides the menu if click happened somewhere outside of the menu.
4104+window.addEvent("click", function (e) {
4105+  var target = e.target;
4106+  if(VISIBLE_MENU && (!target || !$(target).getParents().contains(VISIBLE_MENU._el)))
4107+    VISIBLE_MENU.hide();
4108+});
4109+
4110+})();
4111addfile ./contrib/musicplayer/src/libs/ui/NavigationColumn.js
4112hunk ./contrib/musicplayer/src/libs/ui/NavigationColumn.js 1
4113+//#require "libs/ui/Column.js"
4114+//#require "libs/util/util.js"
4115+
4116+/** section: UserInterface
4117+ *  class da.ui.NavigationColumn < da.ui.Column
4118+ * 
4119+ *  Extends Column class to provide common implementation of a navigation column.
4120+ **/
4121+da.ui.NavigationColumn = new Class({
4122+  Extends: da.ui.Column,
4123
4124+  /**
4125+   *  da.ui.NavigationColumn#view -> {map: $empty, finished: $empty}
4126+   * 
4127+   *  Use this object to pass arguments to `Application.db.view()`.
4128+   * 
4129+   *  If `view.finished` is left empty, it will be replaced with function which will
4130+   *  render the list as soon as map/reduce proccess finishes.
4131+   **/
4132+  view: {
4133+    map: function (doc, emit) {
4134+      if(!this._passesFilter(doc))
4135+        return false;
4136+
4137+      emit(doc.id, {
4138+        title: doc.title || doc.id
4139+      });
4140+    },
4141+   
4142+    finished: $empty
4143+  },
4144
4145+  options: {
4146+    filter: null,
4147+    killView: true
4148+  },
4149
4150+  /**
4151+   *  new da.ui.NavigationColumn([options])
4152+   *  - options.filter (Object | Function): filtering object or function.
4153+   *  - options.db (BrowserCouch): [[BrowserCouch]] database to use for views.
4154+   *    Defaults to `Application.db`.
4155+   * 
4156+   *  If `filter` is provided than it will be applied during the map/reduce proccess.
4157+   *  If it's an [[Object]] than only documents with same properties as those
4158+   *  in `filter` will be considered, and if it's an [[Function]],
4159+   *  than it *must* return `true` if document should be passed to
4160+   *  any aditional filters, or `false` if the document should be discarded.
4161+   *  First argument of the `filter` function will be the document itself.
4162+   * 
4163+   *  If the column lacks map/reduce view but `total_count` is present, [[da.ui.NavigationColumn#render]] will be called.
4164+   * 
4165+   *  All other options are the same as for [[da.ui.Column]].
4166+   **/
4167+  initialize: function (options) {
4168+    this.parent(options);
4169+    this._el.addClass("navigation_column");
4170+   
4171+    // Small speed-hack
4172+    if(!this.options.filter)
4173+      this._passesFilter = $lambda(true);
4174+   
4175+    this._el.addEvent("click:relay(.column_item)", this.click.bind(this));
4176+   
4177+    if(this.view) {
4178+      this.view.map = this.view.map.bind(this);
4179+      if(!this.view.finished || this.view.finished === $empty)
4180+        this.view.finished = this.mapReduceFinished.bind(this);
4181+      else
4182+        this.view.finished = this.view.finished.bind(this);
4183+     
4184+      if(this.view.reduce)
4185+        this.view.reduce = this.view.reduced.bind(this);
4186+      if(!this.view.updated && !this.view.temporary)
4187+        this.view.updated = this.mapReduceUpdated;
4188+      if(this.view.updated)
4189+        this.view.updated = this.view.updated.bind(this);
4190+   
4191+      (options.db || da.db.DEFAULT).view(this.view);
4192+    } else if(this.options.totalCount) {
4193+      this.injectBottom(this.options.parentElement || document.body);
4194+      this.render();
4195+    }
4196+  },
4197
4198+  /**
4199+   *  da.ui.NavigationColumn#mapReduceFinished(values) -> this
4200+   *  - values (Object): an object with result rows and `findRow` function.
4201+   * 
4202+   *  Function called when map/reduce proccess finishes, if not specified otherwise in view.
4203+   *  This function will provide [[da.ui.NavigationColumn#getItem]], update `total_count` option and render the column.
4204+   **/
4205+  mapReduceFinished: function (values) {
4206+    // BrowserCouch's findRow() needs rows to be sorted by id.
4207+    this._rows = $A(values.rows);
4208+    this._rows.sort(this.compareFunction);
4209+   
4210+    this.updateTotalCount(values.rows.length);
4211+    this.injectBottom(this.options.parentElement || document.body);
4212+    return this.render();
4213+  },
4214
4215+  /**
4216+   *  da.ui.NavigationColumn#mapReduceUpdated(values) -> this
4217+   *  - values (Object): rows returned by map/reduce process.
4218+   * 
4219+   *  Note that this will have to re-render the whole column, as it's possible
4220+   *  that one of the new documents should be rendered in the middle of already
4221+   *  rendered ones (due to sorting).
4222+   **/
4223+  mapReduceUpdated: function (values) {
4224+    this._rows = $A(da.db.DEFAULT.views[this.view.id].view.rows);
4225+    this._rows.sort(this.compareFunction);
4226+    this.options.totalCount = this._rows.length;
4227+    return this.rerender();
4228+  },
4229
4230+  /**
4231+   *  da.ui.NavigationColumn#getItem(index) -> Object
4232+   *  - index (Number): index number of the item in the list.
4233+   **/
4234+  getItem: function (index) {
4235+    return this._rows[index];
4236+  },
4237
4238+  /**
4239+   *  da.ui.NavigationColumn#renderItem(index) -> Element
4240+   *  - index (Number): position of the item that needs to be rendered.
4241+   * 
4242+   *  This function relies on `title`, `subtitle` and `icon` properties from emitted documents.
4243+   **/
4244+  renderItem: function (index) {
4245+    var item = this.getItem(index).value,
4246+        el = new Element("a", {href: "#", title: item.title});
4247+   
4248+    if(item.icon)
4249+      el.grab(new Element("img",  {src: item.icon}));
4250+    if(item.title)
4251+      el.grab(new Element("span", {html: item.title,    "class": "title"}));
4252+    if(item.subtitle)
4253+      el.grab(new Element("span", {html: item.subtitle, "class": "subtitle"}));
4254+   
4255+    return el;
4256+  },
4257
4258+  /**
4259+   *  da.ui.NavigationColumn#createFilter(item) -> Object | Function
4260+   *  - item (Object): one of the rendered objects, usually clicked one.
4261+   * 
4262+   *  Returns an object with properties which will be required from
4263+   *  on columns "below" this one.
4264+   * 
4265+   *  If function is returned, than returned function will be called
4266+   *  by Map/Reduce proccess on column "below" and should return `true`/`false`
4267+   *  depending if the document meets criteria.
4268+   * 
4269+   *  #### Examples
4270+   * 
4271+   *      function createFilter (item) {
4272+   *        return {artist_id: item.id};
4273+   *      }
4274+   * 
4275+   **/
4276+  createFilter: function (item) {
4277+   return {};
4278+  },
4279
4280+  click: function (event, el) {
4281+    var item = this.getItem(el.retrieve("column_index"));
4282+    if(this._active_el)
4283+      this._active_el.removeClass("active_column_item");
4284+
4285+    this._active_el = el.addClass("active_column_item");
4286+    this.fireEvent("click", [item, event, el]);
4287+   
4288+    return item;
4289+  },
4290
4291+  /**
4292+   *  da.ui.NavigationColumn#compareFunction(a, b) -> Number
4293+   *  - a (Object): first document.
4294+   *  - b (Object): second document.
4295+   * 
4296+   *  Function used for sorting items returned by map/reduce proccess. Compares documents by their `title` property.
4297+   * 
4298+   *  [See meanings of return values](https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/sort#Description).
4299+   **/
4300+  compareFunction: function (a, b) {
4301+    a = a.value.title;
4302+    b = b.value.title;
4303+   
4304+    if(a < b) return -1;
4305+    if(a > b) return 1;
4306+    return 0;
4307+  },
4308
4309+  destroy: function () {
4310+    this.parent();
4311+    if(this.view)
4312+      if(this.options.killView)
4313+        (this.options.db || da.db.DEFAULT).killView(this.view.id);
4314+      else
4315+        (this.options.db || da.db.DEFAULT).removeEvent("update." + this.view.id, this.view.updated);
4316+  },
4317
4318+  _passesFilter: function (doc) {
4319+    var filter = this.options.filter;
4320+    if(!filter)
4321+      return false;
4322+   
4323+    return (typeof(filter) === "object") ? Hash.containsAll(doc, filter) : filter(doc);
4324+  }
4325+});
4326addfile ./contrib/musicplayer/src/libs/ui/ui.js
4327hunk ./contrib/musicplayer/src/libs/ui/ui.js 1
4328+/**
4329+ *  == UserInterface ==
4330+ * 
4331+ *  Common UI classes like [[Column]] and [[Menu]].
4332+ **/
4333+
4334+/** section: UserInterface
4335+ * da.ui
4336+ **/
4337+da.ui = {};
4338adddir ./contrib/musicplayer/src/libs/util
4339addfile ./contrib/musicplayer/src/libs/util/BinaryFile.js
4340hunk ./contrib/musicplayer/src/libs/util/BinaryFile.js 1
4341+/*
4342+ *  Binary Ajax 0.2
4343+ * 
4344+ *  Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, http://blog.nihilogic.dk/
4345+ *  Copyright (c) 2010 Josip Lisec <josiplisec@gmail.com>
4346+ *  MIT License [http://www.opensource.org/licenses/mit-license.php]
4347+ * 
4348+ *  Adoption for MooTools, da.util.BinaryFile#unpack(), da.util.BinaryFile#getBitsAt() and Request.Binary
4349+ *  were added by Josip Lisec.
4350+ */
4351+
4352+(function () {
4353+/** section: Utilities
4354+ *  class da.util.BinaryFile
4355+ * 
4356+ *  Class containing methods for working with files as binary data.
4357+ **/
4358+var BinaryFile = new Class({
4359+  /**
4360+   *  new da.util.BinaryFile(data[, options])
4361+   *  - data (String): the binary data.
4362+   *  - options.offset (Number): initial offset.
4363+   *  - options.length (Number): length of the data.
4364+   *  - options.bigEndian (Boolean): defaults to `false`.
4365+   **/
4366+  initialize: function (data, options) {
4367+    options = options || {};
4368+    this.data = data;
4369+    this.offset = options.offset || 0;
4370+    this.length = options.length || 0;
4371+    this.bigEndian = options.bigEndian || false;
4372+   
4373+    if(typeof data === "string") {
4374+      this.length = this.length || data.length;
4375+    } else {
4376+      // In this case we're probably dealing with IE,
4377+      // and in order for this to work, VisualBasic-script magic is needed,
4378+      // for which we don't have enough of mana.
4379+      throw Exception("This browser is not supported");
4380+    }
4381+  },
4382
4383+  /**
4384+   *  da.util.BinaryFile#getByteAt(offset) -> Number
4385+   **/
4386+  getByteAt: function (offset) {
4387+    return this.data.charCodeAt(offset + this.offset) & 0xFF;
4388+  },
4389
4390+  /**
4391+   *  da.util.BinaryFile#getSByteAt(offset) -> Number
4392+   **/
4393+  getSByteAt: function(iOffset) {
4394+    var iByte = this.getByteAt(iOffset);
4395+    return iByte > 127 ? iByte - 256 : iByte;
4396+  },
4397+
4398+  /**
4399+   *  da.util.BinaryFile#getShortAt(offset) -> Number
4400+   **/
4401+  getShortAt: function(iOffset) {
4402+    var iShort = this.bigEndian ?
4403+      (this.getByteAt(iOffset) << 8) + this.getByteAt(iOffset + 1)
4404+      : (this.getByteAt(iOffset + 1) << 8) + this.getByteAt(iOffset)
4405+   
4406+    return iShort < 0 ? iShort + 65536 : iShort;
4407+  },
4408
4409+  /**
4410+   *  da.util.BinaryFile#getSShortAt(offset) -> Number
4411+   **/
4412+  getSShortAt: function(iOffset) {
4413+    var iUShort = this.getShortAt(iOffset);
4414+    return iUShort > 32767 ? iUShort - 65536 : iUShort;
4415+  },
4416
4417+  /**
4418+   *  da.util.BinaryFile#getLongAt(offset) -> Number
4419+   **/
4420+  getLongAt: function(iOffset) {
4421+    var iByte1 = this.getByteAt(iOffset),
4422+        iByte2 = this.getByteAt(iOffset + 1),
4423+        iByte3 = this.getByteAt(iOffset + 2),
4424+        iByte4 = this.getByteAt(iOffset + 3);
4425+
4426+    var iLong = this.bigEndian ?
4427+      (((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4
4428+      : (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1;
4429+    if (iLong < 0) iLong += 4294967296;
4430+    return iLong;
4431+  },
4432
4433+  /**
4434+   *  da.util.BinaryFile#getSLongAt(offset) -> Number
4435+   **/
4436+  getSLongAt: function(iOffset) {
4437+    var iULong = this.getLongAt(iOffset);
4438+    return iULong > 2147483647 ? iULong - 4294967296 : iULong;
4439+  },
4440
4441+  /**
4442+   *  da.util.BinaryFile#getStringAt(offset, length) -> String
4443+   **/
4444+  getStringAt: function(offset, length) {
4445+    var str = new Array(length);
4446+    length += offset;
4447+   
4448+    for(var i = 0; offset < length; offset++, i++)
4449+      str[i] = String.fromCharCode(this.getByteAt(offset));
4450+
4451+    return str.join("");
4452+  },
4453+
4454+  /**
4455+   *  da.util.BinaryFile#getCharAt(offset) -> String
4456+   *  - offset (Number): position of the character.
4457+   **/
4458+  getCharAt: function(iOffset) {
4459+    return String.fromCharCode(this.getByteAt(iOffset));
4460+  },
4461
4462+  /**
4463+   *  da.util.BinaryFile#getBitsAt(offset[, length]) -> Array
4464+   *  - offset (Number): position of character.
4465+   *  - length (Number): number of bits, if result has less, zeors will be appended at the begging.
4466+   * 
4467+   *  Returns an array with bit values.
4468+   * 
4469+   *  #### Example
4470+   *      (new da.util.BinaryFile("2")).getBitsAt(0, 8)
4471+   *      // -> [0, 0, 1, 1, 0, 0, 1, 0]
4472+   * 
4473+   **/
4474+  getBitsAt: function (offset, padding) {
4475+    var bits = this.getByteAt(offset).toString(2);
4476+    padding = padding || 8;
4477+    if(padding && bits.length < padding) {
4478+      var delta = padding - bits.length;
4479+      padding = [];
4480+      while(delta--) padding.push(0);
4481+      bits = padding.concat(bits).join("");
4482+    }
4483+   
4484+    var n = bits.length,
4485+        result = new Array(n);
4486+   
4487+    while(n--)
4488+      result[n] = +bits[n];
4489+   
4490+    return result;
4491+  },
4492
4493+  /**
4494+   *  da.util.BinaryFile#getBitsFromStringAt(offset, length) -> Array
4495+   *  - offset (Number): position of the first character.
4496+   *  - length (Number): length of the string.
4497+   * 
4498+   *  Returns an array with return values of [[da.util.BinaryFile#getBitsAt]].
4499+   **/
4500+  getBitsFromStringAt: function (offset, length) {
4501+    var bits = new Array(length);
4502+    length += offset;
4503+   
4504+    for(var i = 0; offset < length; offset++, i++)
4505+      bits[i] = this.getBitsAt(offset);
4506+   
4507+    return bits;
4508+  },
4509
4510+  /**
4511+   *  da.util.BinaryFile#toEncodedString() -> String
4512+   *  Returns URI encoded value of data.
4513+   * 
4514+   *  We're not using from/toBase64 because `btoa()`/`atob()` functions can't convert everything to/from Base64 encoding,
4515+   *  `encodeUriComponent()` method seems to be more reliable.
4516+   **/
4517+  toEncodedString: function() {
4518+    return encodeURIComponent(this.data);
4519+  },
4520
4521+  /**
4522+   *  da.util.BinaryFile#unpack(format) -> Array
4523+   *  - format (String): String according to which data will be unpacked.
4524+   * 
4525+   *  This method is using format similar to the one used in Python, and does exactly the same job,
4526+   *  mapping C types to JavaScript ones.
4527+   * 
4528+   * 
4529+   *  #### Code mapping
4530+   *  <table cellspacing="3px">
4531+   *    <thead style="border-bottom:3px double #ddd">
4532+   *      <tr><td>Code</td><td>C type</td><td>Returns</td><td>Function</td></tr>
4533+   *    </thead>
4534+   *    <tbody>
4535+   *      <tr>
4536+   *        <td>b</td>
4537+   *        <td><code>_Bool</code></td>
4538+   *        <td>[[Boolean]]</td>
4539+   *        <td></td>
4540+   *      </tr>
4541+   *      <tr>
4542+   *        <td>c</td>
4543+   *        <td><code>char</code></td>
4544+   *        <td>[[String]]</td>
4545+   *        <td>String with one character</td>
4546+   *      </tr>
4547+   *      <tr>
4548+   *        <td>h</td>
4549+   *        <td><code>short</code></td>
4550+   *        <td>[[Number]]</td>
4551+   *        <td></td>
4552+   *      </tr>
4553+   *      <tr>
4554+   *        <td>i</td>
4555+   *        <td><code>int</code></td>
4556+   *        <td>[[Number]]</td>
4557+   *        <td></td>
4558+   *      </tr>
4559+   *      <tr>
4560+   *        <td>l</td>
4561+   *        <td><code>long</code></td>
4562+   *        <td>[[Number]]</td>
4563+   *        <td></td>
4564+   *      </tr>
4565+   *      <tr>
4566+   *        <td>s</td>
4567+   *        <td><code>char[]</code></td>
4568+   *        <td>[[String]]</td>
4569+   *        <td></td>
4570+   *      </tr>
4571+   *      <tr>
4572+   *        <td>S</td>
4573+   *        <td><code>char[]</code></td>
4574+   *        <td>[[String]]</td>
4575+   *        <td>String with removed whitespace (including <code>\0</code> chars)</td>
4576+   *      </tr>
4577+   *      <tr>
4578+   *        <td>t</td>
4579+   *        <td><code>int</code></td>
4580+   *        <td>[[Array]]</td>
4581+   *        <td>Returns an array with bit values</td>
4582+   *      </tr>
4583+   *      <tr>
4584+   *        <td>T</td>
4585+   *        <td><code>char</code></td>
4586+   *        <td>[[Array]]</td>
4587+   *        <td>Returns an array of arrays with bit values.</td>
4588+   *      </tr>
4589+   *      <tr>
4590+   *        <td>x</td>
4591+   *        <td><code>/</code></td>
4592+   *        <td>[[String]]</td>
4593+   *        <td>Padding byte</td>
4594+   *      </tr>
4595+   *    </tbody>
4596+   *  </table>
4597+   * 
4598+   * 
4599+   *  #### External resources
4600+   *  * [Python implementation of `unpack`](http://docs.python.org/library/struct.html#format-strings)
4601+   **/
4602+  _unpack_format: /(\d+\w|\w)/g,
4603+  _whitespace: /\s./g,
4604+  unpack: function (format) {
4605+    format = format.replace(this._whitespace, "");
4606+    var pairs = format.match(this._unpack_format),
4607+        n = pairs.length,
4608+        result = [];
4609+   
4610+    if(!pairs.length)
4611+      return pairs;
4612+   
4613+    var offset = 0;
4614+    for(var n = 0, m = pairs.length; n < m; n++) {
4615+      var pair    = pairs[n],
4616+          code    = pair.slice(-1),
4617+          repeat  = +pair.slice(0, pair.length - 1) || 1;
4618+     
4619+      switch(code) {
4620+        case 'b':
4621+          while(repeat--)
4622+            result.push(this.getByteAt(offset++) === 1);
4623+          break;
4624+        case 'c':
4625+          while(repeat--)
4626+            result.push(this.getCharAt(offset++));
4627+          break;
4628+        case 'h':
4629+          while(repeat--) {
4630+            result.push(this.getShortAt(offset));
4631+            offset += 2;
4632+          }
4633+          break;
4634+        case 'i':
4635+          while(repeat--)
4636+            result.push(this.getByteAt(offset++));
4637+          break;
4638+        case 'l':
4639+          while(repeat--) {
4640+            result.push(this.getLongAt(offset));
4641+            offset += 4;
4642+          }
4643+          break;
4644+        case 's':
4645+          result.push(this.getStringAt(offset, repeat));
4646+          offset += repeat;
4647+          break;
4648+        case 'S':
4649+          result.push(this.getStringAt(offset, repeat).strip());
4650+          offset += repeat;
4651+          break;
4652+        case 't':
4653+          while(repeat--)
4654+            result.push(this.getBitsAt(offset++, 2));
4655+          break;
4656+        case 'T':
4657+          result.push(this.getBitsFromStringAt(offset, repeat));
4658+          offset += repeat;
4659+          break;
4660+        case 'x':
4661+          offset += repeat;
4662+          break;
4663+        default:
4664+          throw new Exception("Unknow code is being used (" + code + ").");
4665+      }
4666+    }
4667+   
4668+    return result;
4669+  }
4670+});
4671+
4672+BinaryFile.extend({
4673+  /**
4674+   *  da.util.BinaryFile.fromEncodedString(data) -> da.util.BinaryFile
4675+   *  - data (String): URI encoded string.
4676+   **/
4677+  fromEncodedString: function(encoded_str) {
4678+    return new BinaryFile(decodeURIComponent(encoded_str));
4679+  }
4680+});
4681+
4682+da.util.BinaryFile = BinaryFile;
4683+
4684+/** section: Utilities
4685+ *  class Request
4686+ *
4687+ *  MooTools Request class
4688+ **/
4689+
4690+/** section: Utilities
4691+ *  class Request.Binary < Request
4692+ * 
4693+ *  Class for receiving binary data over XMLHTTPRequest.
4694+ *  If server supports setting Range header, then only minimal data will be downloaded.
4695+ * 
4696+ *  This works in two phases, if a range option is set then a HEAD request is performed to get the
4697+ *  total length of the file and to see if server supports `Range` HTTP header.
4698+ *  If server supports `Range` header than only requested range is asked from server in another HTTP GET request,
4699+ *  otherwise the whole file is downloaded and sliced to desired range.
4700+ * 
4701+ **/
4702+Request.Binary = new Class({
4703+  Extends: Request,
4704
4705+  /**
4706+   *  Request.Binary#acceptsRange -> String
4707+   *  Indicates if server supports HTTP requests with `Range` header.
4708+   **/
4709+  acceptsRange: false,
4710+  options: {
4711+    range: null
4712+  },
4713
4714+  /**
4715+   *  new Request.Binary(options)
4716+   *  - options (Object): all of the [Request](http://mootools.net/docs/core/Request/Request) options can be used.
4717+   *  - options.range (Object): array with starting position and length. `[0, 100]`.
4718+   *    If first element is negative, starting position will be calculated from end of the file.
4719+   *  - options.bigEndian (Boolean)
4720+   *  fires request, complete, success, failure, cancel
4721+   * 
4722+   *  Functions attached to `success` event will receive response in form of [[da.util.BinaryFile]] as their first argument.
4723+   **/
4724+  initialize: function (options) {
4725+    this.parent($extend(options, {
4726+      method: "GET"
4727+    }));
4728+
4729+    this.headRequest = new Request({
4730+      url:          options.url,
4731+      method:       "HEAD",
4732+      emulation:    false,
4733+      evalResponse: false,
4734+      onSuccess:    this.onHeadSuccess.bind(this)
4735+    });
4736+  },
4737
4738+  onHeadSuccess: function () {
4739+    this.acceptsRange = this.headRequest.getHeader("Accept-Ranges") === "bytes";
4740+   
4741+    var range = this.options.range;
4742+    if(range[0] < 0)
4743+      range[0] += +this.headRequest.getHeader("Content-Length");
4744+    range[1] = range[0] + range[1] - 1;
4745+    this.options.range = range;
4746+
4747+    if(this.headRequest.isSuccess())
4748+      this.send(this._send_options || {});
4749+  },
4750
4751+  success: function (text) {
4752+    var range = this.options.range;
4753+    this.response.binary = new BinaryFile(text, {
4754+      offset: range && !this.acceptsRange ? range[0] : 0,
4755+      length: range ? range[1] - range[0] + 1 : 0,
4756+      bigEndian: this.options.bigEndian
4757+    });
4758+    this.onSuccess(this.response.binary);
4759+  },
4760
4761+  send: function (options) {
4762+    if(this.headRequest.running || this.running)
4763+      return this;
4764+
4765+    if(!this.headRequest.isSuccess()) {
4766+      this._send_options = options;
4767+      this.headRequest.send();
4768+      return this;
4769+    }
4770+   
4771+    if(typeof this.xhr.overrideMimeType === "function")
4772+      this.xhr.overrideMimeType("text/plain; charset=x-user-defined");
4773+
4774+    this.setHeader("If-Modified-Since", "Sat, 1 Jan 1970 00:00:00 GMT");
4775+    var range = this.options.range;
4776+    if(range && this.acceptsRange)
4777+      this.setHeader("Range", "bytes=" + range[0] + "-" + range[1]);
4778+   
4779+    return this.parent(options);
4780+  }
4781+});
4782+
4783+})();
4784addfile ./contrib/musicplayer/src/libs/util/Goal.js
4785hunk ./contrib/musicplayer/src/libs/util/Goal.js 1
4786+/** section: Utilities
4787+ *  class da.util.Goal
4788+ *  implements Events, Options
4789+ * 
4790+ *  A helper class which makes it easier to manage async nature of JS.
4791+ *  An Goal consists of several checkpoints, which, in order to complete the goal have to be reached.
4792+ * 
4793+ *  #### Examples
4794+ *   
4795+ *      var travel_the_world = new da.util.Goal({
4796+ *        checkpoints: ["Nicosia", "Vienna", "Berlin", "Paris", "London", "Reykjavik"],
4797+ *       
4798+ *        onCheckpoint: function (city) {
4799+ *          console.log("Hello from " + name + "!");
4800+ *        },
4801+ *       
4802+ *        onFinish: function () {
4803+ *          console.log("Yay!");
4804+ *        },
4805+ *       
4806+ *        afterCheckpoint: {
4807+ *          Paris: function () {
4808+ *            consle.log("Aww...");
4809+ *          }
4810+ *        }
4811+ *      });
4812+ *     
4813+ *      travel_the_world.checkpoint("Nicosia");
4814+ *      // -> "Hello from Nicosia!"
4815+ *      travel_the_world.checkpoint("Berlin");
4816+ *      // -> "Hello from Berlin!"
4817+ *      travel_the_world.checkpoint("Paris");
4818+ *      // -> "Hello from Paris!"
4819+ *      // -> "Aww..."
4820+ *      travel_the_world.checkpoint("London");
4821+ *      // -> "Hello from London!"
4822+ *      travel_the_world.checkpoint("Reykyavik");
4823+ *      // -> "Hello from Paris!"
4824+ *      travel_the_world.checkpoint("Vienna");
4825+ *      // -> "Hello from Vienna!"
4826+ *      // -> "Yay!"
4827+ *   
4828+ **/
4829+da.util.Goal = new Class({
4830+  Implements: [Events, Options],
4831
4832+  options: {
4833+    checkpoints: [],
4834+    afterCheckpoint: {}
4835+  },
4836+  /**
4837+   *  da.util.Goal#finished -> Boolean
4838+   * 
4839+   *  Indicates if all checkpoints have been reached.
4840+   **/
4841+  finished: false,
4842
4843+  /**
4844+   *  new da.util.Goal([options])
4845+   *  - options.checkpoints (Array): list of checkpoints needed for goal to finish.
4846+   *  - options.onFinish (Function): called once all checkpoints are reached.
4847+   *  - options.onCheckpoint (Function): called after each checkpoint.
4848+   *  - options.afterCheckpoint (Object): object keys represent checkpoints whose functions will be called after respective checkpoint.
4849+   **/
4850+  initialize: function (options) {
4851+    this.setOptions(options);
4852+    this.completedCheckpoints = [];
4853+  },
4854
4855+  /**
4856+   *  da.util.Goal#checkpoint(name) -> undefined | false
4857+   *  - name (String): name of the checkpoint.
4858+   *  fires checkpoint, finish
4859+   * 
4860+   *  Registers that checkpoint has been reached;
4861+   **/
4862+  checkpoint: function (name) {
4863+    if(!this.options.checkpoints.contains(name))
4864+      return false;
4865+    if(this.completedCheckpoints.contains(name))
4866+      return false;
4867+   
4868+    this.completedCheckpoints.push(name);
4869+    this.fireEvent("checkpoint", [name, this.completedCheckpoints]);
4870+   
4871+    if(this.options.afterCheckpoint[name])
4872+      this.options.afterCheckpoint[name](this.completedCheckpoints);
4873+   
4874+    if(this.completedCheckpoints.containsAll(this.options.checkpoints))
4875+      this.finish();
4876+  },
4877
4878+  finish: function () {
4879+    this.finished = true;
4880+    this.fireEvent("finish");
4881+  }
4882+});
4883addfile ./contrib/musicplayer/src/libs/util/ID3.js
4884hunk ./contrib/musicplayer/src/libs/util/ID3.js 1
4885+/**
4886+ *  == ID3 ==
4887+ * 
4888+ *  ID3 parsers and common interface.
4889+ **/
4890+
4891+/** section: ID3
4892+ *  class da.util.ID3
4893+ * 
4894+ *  Class for extracting ID3 metadata from music files. Provides an interface to ID3v1 and ID3v2 parsers.
4895+ *  The reason why ID3 v1 and v2 parsers are implemented separately is due to idea that parsers for other
4896+ *  formats (OGG Comments, especially) can be later implemented with ease.
4897+**/
4898+da.util.ID3 = new Class({
4899+  Implements: Options,
4900
4901+  options: {
4902+    url: null,
4903+    onSuccess: $empty,
4904+    onFailure: $empty
4905+  },
4906
4907+  /**
4908+   *  da.util.ID3#parsers -> Array
4909+   *  List of parsers with which the file will be tested. Defaults to ID3v2 and ID3v1 parsers.
4910+   **/
4911+  parsers: [],
4912
4913+  /**
4914+   *  da.util.ID3#parser -> Object
4915+   * 
4916+   *  Instance of the parser in use.
4917+  **/ 
4918+  parser: null,
4919
4920+  /**
4921+   *  new da.util.ID3(options)
4922+   *  - options.url (String): URL of the MP3 file.
4923+   *  - options.onSuccess (Function): called with found tags once they are parsed.
4924+   *  - options.onFailure (Function): called if none of available parsers know how to extract tags.
4925+   * 
4926+   **/
4927+  initialize: function (options) {
4928+    this.setOptions(options);
4929+    this.parsers = $A(da.util.ID3.parsers);
4930+    this._getFile(this.parsers[0]);
4931+  },
4932
4933+  _getFile: function (parser) {
4934+    if(!parser)
4935+      return this.options.onFailure();
4936+   
4937+    this.request = new Request.Binary({
4938+      url: this.options.url,
4939+      range: parser.range,
4940+      onSuccess: this._onFileFetched.bind(this)
4941+    });
4942+   
4943+    this.request.send();
4944+  },
4945
4946+  _onFileFetched: function (data) {
4947+    if(this.parsers[0] && this.parsers[0].test(data))
4948+      this.parser = (new this.parsers[0](data, this.options, this.request));
4949+    else
4950+      this._getFile(this.parsers.shift());
4951+  }
4952+});
4953+
4954+/**
4955+ *  da.util.ID3.parsers -> Array
4956+ *  Array with all known parsers.
4957+ **/
4958+
4959+da.util.ID3.parsers = [];
4960+
4961+//#require "libs/util/ID3v2.js"
4962+//#require "libs/util/ID3v1.js"
4963addfile ./contrib/musicplayer/src/libs/util/ID3v1.js
4964hunk ./contrib/musicplayer/src/libs/util/ID3v1.js 1
4965+//#require "libs/util/BinaryFile.js"
4966+
4967+/** section: ID3
4968+ *  class da.util.ID3v1Parser
4969+ * 
4970+ *  ID3 v1 parser based on [ID3 v1 specification](http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm#MPEGTAG).
4971+ * 
4972+ *  #### Notes
4973+ *  All of these methods are private.
4974+ **/
4975+
4976+(function () {
4977+var CACHE = {};
4978+
4979+var ID3v1Parser = new Class({
4980+  /**
4981+   *  new da.util.ID3v1Parser(data, options)
4982+   *  - data (da.util.BinaryFile): ID3 tag.
4983+   *  - options.url (String): URL of the file.
4984+   *  - options.onSuccess (Function): function called once tags are parsed.
4985+   **/
4986+  initialize: function (data, options) {
4987+    this.data = data;
4988+    this.options = options;
4989+    if(!this.options.url)
4990+      this.options.url = Math.uuid();
4991+   
4992+    if(CACHE[options.url])
4993+      options.onSuccess(CACHE[options.url]);
4994+    else
4995+      this.parse();
4996+  },
4997
4998+  /**
4999+   *  da.util.ID3v1Parser#parse() -> undefined
5000+   *  Extracts the tags from file.
5001+   **/
5002+  parse: function () {
5003+    // 29x - comment
5004+    this.tags = this.data.unpack("xxx30S30S30S4S29x2i").associate([
5005+      "title", "artist", "album", "year", "track", "genre"
5006+    ]);
5007+    this.tags.year = +this.tags.year;
5008+    if(isNaN(this.tags.year))
5009+      this.tags.year = 0;
5010+   
5011+    this.options.onSuccess(CACHE[this.options.url] = this.tags);
5012+  }
5013+});
5014+
5015+ID3v1Parser.extend({
5016+  /**
5017+   *  da.util.ID3v1Parser.range -> [-128, 128]
5018+   *  Range in which ID3 tag is positioned. -128 indicates that it's last 128 bytes.
5019+   **/
5020+  range: [-128, 128],
5021
5022+  /**
5023+   *  da.util.ID3v1Parser.test(data) -> Boolean
5024+   *  - data (da.util.BinaryFile): data that needs to be tested.
5025+   * 
5026+   *  Checks if first three characters equal to `TAG`, as per ID3 v1 specification.
5027+   **/
5028+  test: function (data) {
5029+    return data.getStringAt(0, 3) === "TAG";
5030+  }
5031+});
5032+
5033+da.util.ID3v1Parser = ID3v1Parser;
5034+da.util.ID3.parsers.push(ID3v1Parser);
5035+})();
5036addfile ./contrib/musicplayer/src/libs/util/ID3v2.js
5037hunk ./contrib/musicplayer/src/libs/util/ID3v2.js 1
5038+//#require "libs/util/BinaryFile.js"
5039+/** section: ID3
5040+ *  class da.util.ID3v2Parser
5041+ * 
5042+ *  ID3 v2 parser implementation based on [Mutagen](http://code.google.com/p/mutagen) and
5043+ *  [ruby-mp3info](http://ruby-mp3info.rubyforge.org) libraries.
5044+ * 
5045+ *  #### Known frames
5046+ *  This is the list of frames that this implementation by default can parse - only those that are needed to get
5047+ *  the basic information about song. Others can be added via da.util.ID3v2Parser.addFrameParser.
5048+ * 
5049+ *  * TRCK
5050+ *  * TIT1
5051+ *  * TIT2
5052+ *  * TIT3
5053+ *  * TPE1
5054+ *  * TPE2
5055+ *  * TALB
5056+ *  * TYER
5057+ *  * TIME
5058+ *  * TCON
5059+ *  * USLT
5060+ *  * WOAR
5061+ *  * WXXX
5062+ * 
5063+ *  As well as their equivalents in ID3 v2.2 specification.
5064+ * 
5065+ *  #### Notes
5066+ *  All methods except for `addFrameParser` are private.
5067+ * 
5068+ *  #### External resources
5069+ *  * [ID3v2.4 specification](http://www.id3.org/id3v2.4.0-structure)
5070+ *  * [ID3v2.4 native frames](http://www.id3.org/id3v2.4.0-frames)
5071+ *  * [ID3v2.3 specification](http://www.id3.org/id3v2.3.0)
5072+ *  * [ID3v2.2 specification](http://www.id3.org/id3v2-00) -- obsolete
5073+ **/
5074+
5075+(function () {
5076+/** section: ID3
5077+ * da.util.ID3v2Parser.frameTypes
5078+ *
5079+ *  Contains know ID3v2 frame types.
5080+ **/
5081+var BinaryFile = da.util.BinaryFile,
5082+    CACHE = [],
5083+FrameType = {
5084+  /**
5085+   *  da.util.ID3v2Parser.frameTypes.text(offset, size) -> String
5086+   **/
5087+  text: function (offset, size) {
5088+    var d = this.data;
5089+    if(d.getByteAt(offset) === 1) {
5090+      // Unicode is being used, and we're trying to detect Unicode BOM.
5091+      // (we don't actually care if it's little or big endian)
5092+      if(d.getByteAt(offset + 1) + d.getByteAt(offset + 2) === 255 + 254) {
5093+        offset += 2;
5094+        size -= 2;
5095+      }
5096+    }
5097+   
5098+    return d.getStringAt(offset + 1, size - 1).strip();
5099+  },
5100
5101+  /**
5102+   *  da.util.ID3v2Parser.frameTypes.textNumeric(offset, size) -> String
5103+   **/
5104+  textNumeric: function(offset, size) {
5105+    return +FrameType.text.call(this, offset, size);
5106+  },
5107
5108+  /**
5109+   *  da.util.ID3v2Parser.frameTypes.link(offset, size) -> String
5110+   **/
5111+  link: function (offset, size) {
5112+    return this.data.getStringAt(offset, size).strip();
5113+  },
5114
5115+  /**
5116+   *  da.util.ID3v2Parser.frameTypes.userLink(offset, size) -> String
5117+   **/
5118+  userLink: function (offset, size) {
5119+    var str = this.data.getStringAt(offset, size);
5120+    return str.slice(str.lastIndexOf("\0") + 1);
5121+  },
5122
5123+  /**
5124+   *  da.util.ID3v2Parser.frameTypes.unsyncedLyrics(offset, size) -> String
5125+   **/
5126+  unsyncedLyrics: function (offset, size) {
5127+    var is_utf8 = this.data.getByteAt(offset) === 1,
5128+        lang    = this.data.getStringAt(offset += 1, 3);
5129+   
5130+    return this.data.getStringAt(offset += 3, size - 4).strip();
5131+  },
5132
5133+  ignore: $empty
5134+},
5135+FRAMES = {
5136+  // ID3v2.4 tags
5137+  SEEK: $empty,
5138
5139+  // ID3v2.3 tags
5140+  TRCK: function (offset, size) {
5141+    var data = FrameType.text.call(this, offset, size);
5142+    return +data.split("/")[0]
5143+  },
5144+  TIT1: FrameType.text,
5145+  TIT2: FrameType.text,
5146+  TIT3: FrameType.text,
5147+  TPE1: FrameType.text,
5148+  TPE2: FrameType.text,
5149+  TALB: FrameType.text,
5150+  TYER: FrameType.textNumeric,
5151+  TIME: $empty,
5152+  TCON: function (offset, size) {
5153+    // Genre, can be either "(123)Genre", "(123)" or "Genre".
5154+    var data = FrameType.text.call(this, offset, size);
5155+    return +((data.match(/^\(\d+\)/) || " ")[0].slice(1, -1));
5156+  },
5157+  USLT: FrameType.unsyncedLyrics,
5158+  WOAR: FrameType.link,
5159+  WXXX: FrameType.userLink
5160+};
5161+
5162+// ID3v2.2 tags (the structure is the same as in later versions, but they use different names)
5163+$extend(FRAMES, {
5164+  UFI: FRAMES.UFID,
5165+  TT1: FRAMES.TIT1,
5166+  TT2: FRAMES.TIT2,
5167+  TT3: FRAMES.TIT3,
5168+  TP1: FRAMES.TPE1,
5169+  TP2: FRAMES.TPE2,
5170+  TP3: FRAMES.TPE3,
5171+  TP4: FRAMES.TPE4,
5172+  TAL: FRAMES.TALB,
5173+  TRK: FRAMES.TRCK,
5174+  TYE: FRAMES.TYER,
5175+  TPB: FRAMES.TPUB,
5176+  ULT: FRAMES.USLT,
5177+  WAR: FRAMES.WOAR,
5178+  WXX: FRAMES.WXXX
5179+});
5180+
5181+var ID3v2Parser = new Class({
5182+  /**
5183+   *  new da.util.ID3v2Parser(data, options, request)
5184+   *  - data (BinaryFile): tag.
5185+   *  - options.onSuccess (Function): function which will be called once tag is parsed.
5186+   *  - request (Request.Binary): original HTTP request object.
5187+   **/
5188+  initialize: function (data, options, request) {
5189+    this.options = options;
5190+   
5191+    this.data = data;
5192+    this.data.bigEndian = true;
5193+   
5194+    this.header = {};
5195+    this.frames = {};
5196+   
5197+    this._request = request;
5198+   
5199+    if(CACHE[options.url])
5200+      options.onSuccess(CACHE[options.url]);
5201+    else
5202+      this.parse();
5203+  },
5204
5205+  /**
5206+   *  da.util.ID3v2Parser#parse() -> undefined
5207+   *  Parses the tag. If size of tag exceeds current data (and it usually does)
5208+   *  another HTTP GET request is issued to get the rest of the file.
5209+   **/
5210+  /**
5211+   *  da.util.ID3v2Parser#header -> {majorVersion: 0, minorVersion: 0, flags: 0, size: 0}
5212+   *  Parsed ID3 header.
5213+   **/
5214+  /**
5215+   *  da.util.ID3v2Parser#version -> 2.2 | 2.3 | 2.4
5216+   **/
5217+  parse: function () {
5218+    this.header = this.data.unpack("xxx2ii4s").associate([
5219+      'majorVersion', 'minorVersion', "flags", "size"
5220+    ]);
5221+    this.version = 2 + (this.header.majorVersion/10) + this.header.minorVersion;
5222+    this.header.size = this.unsync(this.header.size) + 10;
5223+   
5224+    this.parseFlags();
5225+   
5226+    if(this.data.length >= this.header.size)
5227+      return this.parseFrames();
5228+   
5229+    this._request.options.range = [0, this.header.size];
5230+    // Removing event listeners which were added by ID3
5231+    this._request.removeEvents('success');
5232+    this._request.addEvent('success', function (data) {
5233+      this.data = data;
5234+      this.parseFrames();
5235+    }.bind(this));
5236+    this._request.send();
5237+  },
5238
5239+  /**
5240+   *  da.util.ID3v2Parser#parseFlags() -> undefined
5241+   *  Parses header flags.
5242+   **/
5243+   /**
5244+    *  da.util.ID3v2Parser#flags -> {unsync_all: false, extended: false, experimental: false, footer: false}
5245+    *  Header flags.
5246+   **/
5247+  parseFlags: function () {
5248+    var flags = this.header.flags;
5249+    this.flags = {
5250+      unsync_all:   flags & 0x80,
5251+      extended:     flags & 0x40,
5252+      experimental: flags & 0x20,
5253+      footer:       flags & 0x10
5254+    };
5255+  },
5256+
5257+  /**
5258+   *  da.util.ID3v2Parser#parseFrames() -> undefined
5259+   *  Calls proper function for parsing frames depending on tag's version.
5260+   **/
5261+  parseFrames: function () {
5262+    if(this.version >= 2.3)
5263+      this.parseFrames_23();
5264+    else
5265+      this.parseFrames_22();
5266+   
5267+    CACHE[this.options.url] = this.frames;
5268+    this.options.onSuccess(this.simplify(), this.frames);
5269+  },
5270+
5271+  /**
5272+   *  da.util.ID3v2Parser#parseFrames_23() -> undefined
5273+   *  Parses ID3 frames from ID3 v2.3 and newer.
5274+   **/
5275+  parseFrames_23: function () {
5276+    if(this.version >= 2.4 && this.flags.unsync_all)
5277+      this.data.data = this.unsync(0, this.header.size);
5278+
5279+    var offset = 10,
5280+        ext_header_size = this.data.getStringAt(offset, 4),
5281+        tag_size = this.header.size;
5282+
5283+    // Some tagging software is apparently know for setting
5284+    // "extended header present" flag but then ommiting it from the file,
5285+    // which means that ext_header_size will be equal to name of a frame.
5286+    if(this.flags.extended && !FRAMES[ext_header_size]) {
5287+      if(this.version >= 2.4)
5288+        ext_header_size = this.unsync(ext_header_size) - 4;
5289+      else
5290+        ext_header_size = this.data.getLongAt(10);
5291+     
5292+      offset += ext_header_size;
5293+    }
5294+   
5295+    while(offset < tag_size) {
5296+      var foffset     = offset,
5297+          frame_name  = this.data.getStringAt(foffset, 4),
5298+          frame_size  = this.unsync(foffset += 4, 4),
5299+          frame_flags = [this.data.getByteAt(foffset += 4), this.data.getByteAt(foffset += 1)];
5300+      foffset++; // frame_flags
5301+     
5302+      if(!frame_size)
5303+        break;
5304+     
5305+      if(FRAMES[frame_name] && frame_size)
5306+        this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size);
5307+     
5308+      //console.log(frame_name, this.frames[frame_name], [foffset, frame_size]);
5309+      offset += frame_size + 10;
5310+    }
5311+  },
5312
5313+  /**
5314+   *  da.util.ID3v2Parser#parseFrames_22() -> undefined
5315+   *  Parses ID3 frames from ID3 v2.2 tags.
5316+   **/
5317+  parseFrames_22: function () {
5318+    var offset = 10,
5319+        tag_size = this.header.size;
5320+   
5321+    while(offset < tag_size) {
5322+      var foffset = offset,
5323+          frame_name = this.data.getStringAt(foffset, 3),
5324+          frame_size = (new BinaryFile(
5325+            "\0" + this.data.getStringAt(foffset += 3, 3),
5326+            {bigEndian:true}
5327+          )).getLongAt(0);
5328+      foffset += 3;
5329+     
5330+      if(!frame_size)
5331+        break;
5332+     
5333+      if(FRAMES[frame_name] && frame_size)
5334+        this.frames[frame_name] = FRAMES[frame_name].call(this, foffset, frame_size);
5335+     
5336+      //console.log(frame_name, this.frames[frame_name], [foffset, frame_size]);
5337+      offset += frame_size + 6;
5338+    }
5339+  },
5340+
5341+  /**
5342+   *  da.util.ID3v2Parser#unsync(offset, length[, bits = 7]) -> Number
5343+   *  da.util.ID3v2Parser#unsync(string) -> Number
5344+   *  - offset (Number): offset from which so start unsyncing.
5345+   *  - length (Number): length string to unsync.
5346+   *  - bits (Number): number of bits used.
5347+   *  - string (String): String to unsync.
5348+   * 
5349+   *  Performs unsyncing process defined in ID3 specification.
5350+   **/
5351+  unsync: function (offset, length, bits) {
5352+    bits = bits || 7;
5353+    var mask = (1 << bits) - 1,
5354+        bytes = [],
5355+        numeric_value = 0,
5356+        data = this.data;
5357+   
5358+    if(typeof offset === "string") {
5359+      data = new BinaryFile(offset, {bigEndian: true});
5360+      length = offset.length;
5361+      offset = 0;
5362+    }
5363+   
5364+    if(length) {
5365+      for(var n = offset, m = offset + length; n < m; n++)
5366+        bytes.push(data.getByteAt(n) & mask);
5367+     
5368+      bytes.reverse();
5369+    } else {
5370+      var value = data.getByteAt(offset);
5371+      while(value) {
5372+        bytes.push(value & mask);
5373+        value >>= 8;
5374+      }
5375+    }
5376+   
5377+    for(var n = 0, i = 0, m = bytes.length * bits; n < m; n+=bits, i++)
5378+      numeric_value += bytes[i] << n;
5379+   
5380+    return numeric_value;
5381+  },
5382
5383+  /**
5384+   *  da.util.ID3v2Parser#simplify() -> Object
5385+   * 
5386+   *  Returns humanised version of data parsed from frames.
5387+   *  Returned object contains these values (in brackets are used frames or default values):
5388+   * 
5389+   *  * title (`TIT2`, `TT2`, `"Unknown"`)
5390+   *  * album (`TALB`, `TAL`, `"Unknown"`)
5391+   *  * artist (`TPE2`, `TPE1`, `TP2`, `TP1`, `"Unknown"`)
5392+   *  * track (`TRCK`, `TRK`, `0`)
5393+   *  * year (`TYER`, `TYE`, `0`)
5394+   *  * genre (`TCON`, `TCO`, `0`)
5395+   *  * lyrics (`USLT`, `ULT`, _empty string_)
5396+   *  * links: official (`WOAR`, `WXXX`, `WAR`, `WXXX`, _empty string_)
5397+   **/
5398+  simplify: function () {
5399+    var f = this.frames;
5400+    return !f || !$H(f).getKeys().length ? {} : {
5401+      title:  f.TIT2 || f.TT2 || "Unknown",
5402+      album:  f.TALB || f.TAL || "Unknown",
5403+      artist: f.TPE2 || f.TPE1 || f.TP2 || f.TP1 || "Unknown",
5404+      track:  f.TRCK || f.TRK || 0,
5405+      year:   f.TYER || f.TYE || 0,
5406+      genre:  f.TCON || f.TCO || 0,
5407+      lyrics: f.USLT || f.ULT || "",
5408+      links: {
5409+        official: f.WOAR || f.WXXX || f.WAR || f.WXX || ""
5410+      }
5411+    };
5412+  }
5413+});
5414+
5415+ID3v2Parser.extend({
5416+  /**
5417+   *  da.util.ID3v2Parser.range -> [0, 14]
5418+   * 
5419+   *  Default position of ID3v2 header, including extended header.
5420+   **/
5421+  range: [0, 10 + 4],
5422
5423+  /**
5424+   *  da.util.ID3v2Parser.test(data) -> Boolean
5425+   *  - data (BinaryFile): the tag.
5426+   * 
5427+   *  Checks if data begins with `ID3` and major version is less than 5.
5428+  **/
5429+  test: function (data) {
5430+    return data.getStringAt(0, 3) === "ID3" && data.getByteAt(3) <= 4;
5431+  },
5432
5433+  /**
5434+   *  da.util.ID3v2Parser.addFrameParser(frameName, fn) -> da.util.ID3v2Parser
5435+   *  - frameName (String): name of the frame.
5436+   *  - fn (Function): function which will parse the data.
5437+   * 
5438+   *  Use this method to add your own ID3v2 frame parsers. You can access this as `da.util.ID3v2Parser.addFrameParser`.
5439+   * 
5440+   *  `fn` will be called with following arguments:
5441+   *  * offset - position at frame appears in data
5442+   *  * size - size of the frame, including header
5443+   * 
5444+   * 
5445+   *  `this` keyword inside `fn` will refer to instance of ID3v2.
5446+   **/
5447+  addFrameParser: function (name, fn) {
5448+    FRAMES[name] = fn;
5449+    return this;
5450+  }
5451+});
5452+
5453+ID3v2Parser.frameTypes = FrameType;
5454+da.util.ID3v2Parser = ID3v2Parser;
5455+da.util.ID3.parsers.push(ID3v2Parser);
5456+
5457+})();
5458addfile ./contrib/musicplayer/src/libs/util/util.js
5459hunk ./contrib/musicplayer/src/libs/util/util.js 1
5460+/**
5461+ *  == Utilities ==
5462+ *  Utility classes and extensions to Native objects.
5463+ **/
5464+
5465+/**
5466+ * da.util
5467+ **/
5468+if(typeof da.util === "undefined")
5469+  da.util = {};
5470+
5471+(function () {
5472+
5473+/** section: Utilities
5474+ *  class String
5475+ * 
5476+ *  #### External resources
5477+ *  * [MooTools String docs](http://mootools.net/docs/core/Native/String)
5478+ **/
5479+var NULL_BYTE = /\0/g,
5480+    INTERPOL_VAR = /\{(\w+)\}/g;
5481+
5482+String.implement({
5483+  /**
5484+   *  String.strip(@string) -> String
5485+   * 
5486+   *  Removes \0's from string.
5487+   **/
5488+  strip: function () {
5489+    return this.replace(NULL_BYTE, "");
5490+  },
5491
5492+  /**
5493+   *  String.interpolate(@string, data) -> String
5494+   *  - data (Object | Array): object or an array with data.
5495+   * 
5496+   *  Interpolates string with data.
5497+   * 
5498+   *  #### Example
5499+   * 
5500+   *      "{0}/{1}%".interpolate([10, 100])
5501+   *      // -> "10/100%"
5502+   *     
5503+   *      "Hi {name}! You've got {new_mail} new messages.".interpolate({name: "John", new_mail: 10})
5504+   *      // -> "Hi John! You've got 10 new messages."
5505+   * 
5506+   **/
5507+  interpolate: function (data) {
5508+    if(!data)
5509+      return this.toString(); // otherwise typeof result === "object".
5510+   
5511+    return this.replace(INTERPOL_VAR, function (match, property) {
5512+      var value = data[property];
5513+      return typeof value === "undefined" ? "{" + property + "}" : value;
5514+    });
5515+  }
5516+});
5517+
5518+/** section: Utilities
5519+ *  class Array
5520+ * 
5521+  *  #### External resources
5522+  *  * [MooTools Array docs](http://mootools.net/docs/core/Native/Array)
5523+  *  * [MDC Array specification](https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array)
5524+ **/
5525+Array.implement({
5526+  /**
5527+   *  Array.zip(@array...) -> Array
5528+   * 
5529+   *  Returns an array whose n-th element contains n-th element from each argument.
5530+   * 
5531+   *  #### Example
5532+   *      Array.zip([1,2,3], [1,2,3])
5533+   *      // -> [[1, 1], [2, 2], [3, 3]]
5534+   * 
5535+   *  #### See also
5536+   *  * [Python's `zip` function](http://docs.python.org/library/functions.html?highlight=zip#zip)
5537+   **/
5538+  zip: function () {
5539+    var n = this.length,
5540+        args = [this].concat($A(arguments));
5541+        args_length = args.length,
5542+        zipped = new Array(n);
5543+   
5544+    while(n--) {
5545+      zipped[n] = new Array(args_length);
5546+      var m = args_length;
5547+      while(m--)
5548+        zipped[n][m] = args[m][n];
5549+    }
5550+   
5551+    return zipped;
5552+  },
5553
5554+  /**
5555+   *  Array.containsAll(@array, otherArray) -> Boolean
5556+   *  - otherArray (Array): array which has to contain all of the defined items.
5557+   * 
5558+   *  Checks if this array contains all of those provided in otherArray.
5559+   **/
5560+   containsAll: function (other) {
5561+     var n = other.length;
5562+     
5563+     while(n--)
5564+      if(!this.contains(other[n]))
5565+        return false;
5566+   
5567+    return true;
5568+   }
5569+});
5570+
5571+/** section: Utilities
5572+ *  class Hash
5573+ * 
5574+ *  #### External resources
5575+ *  * [MooTools Hash docs](http://mootools.net/docs/core/Native/Hash)
5576+ **/
5577+
5578+Hash.implement({
5579+  /**
5580+   *  Hash.containsAll(@hash, otherHash) -> Boolean
5581+   *  - otherHash (Hash | Object): hash which has to contain all of the defined properties.
5582+   * 
5583+   *  Checks if all properties from this hash are present in otherHash.
5584+   **/
5585+  containsAll: function (otherHash) {
5586+    for(var key in otherHash)
5587+      if(otherHash.hasOwnProperty(key) && otherHash[key] !== this[key])
5588+        return false;
5589+   
5590+    return true;
5591+  }
5592+})
5593+
5594+})();
5595adddir ./contrib/musicplayer/src/resources
5596adddir ./contrib/musicplayer/src/resources/css
5597addfile ./contrib/musicplayer/src/resources/css/app.css
5598hunk ./contrib/musicplayer/src/resources/css/app.css 1
5599+/*** Global styles ***/
5600+@font-face {
5601+  font-family: Junction;
5602+  font-style: normal;
5603+  font-weight: normal;
5604+  src: local('Junction'), url('resources/fonts/Junction.ttf') format('truetype');
5605+}
5606+
5607+body {
5608+  font-family: 'Liberation Sans', 'Helvetica Neue', Helvetica, sans-serif;
5609+  overflow: hidden;
5610+}
5611+
5612+a {
5613+  text-decoration: none;
5614+  color: inherit;
5615+}
5616+
5617+input[type="text"], input[type="password"] {
5618+  border: 1px solid #ddd;
5619+  border-top: 1px solid #c0c0c0;
5620+  background: #fff;
5621+  padding: 2px;
5622+}
5623+
5624+input:focus, input:active {
5625+  border-color: #33519d;
5626+  -webkit-box-shadow: #33519d 0 0 5px;
5627+  -moz-box-shadow: #33519d 0 0 5px;
5628+  -o-box-shadow: #33519d 0 0 5px;
5629+  box-shadow: #33519d 0 0 5px;
5630+}
5631+
5632+input[type="button"], input[type="submit"], button {
5633+  background: #ddd;
5634+  border: 1px transparent;
5635+  border-bottom: 1px solid #c0c0c0;
5636+  padding: 2px 7px;
5637+  color: #000;
5638+  text-shadow: #fff 0 1px 0;
5639
5640+  -webkit-border-radius: 4px;
5641+  -moz-border-radius: 4px;
5642+  -o-border-radius: 4px;
5643+  border-radius: 4px;
5644+}
5645+
5646+input[type="button"]:active, input[type="submit"]:active, button:active {
5647+  border-top: 1px solid #1e2128;
5648+  border-bottom: 0;
5649+  background: #33519d !important;
5650+  color: #fff;
5651+  text-shadow: #000 0 1px 1px;
5652+}
5653+
5654+.no_selection {
5655+  -webkit-user-select: none;
5656+  -moz-user-select: none;
5657+  -o-user-select: none;
5658+  user-select: none;
5659+  cursor: default;
5660+}
5661+
5662+/*** Dialogs ***/
5663+.dialog_wrapper {
5664+  width: 100%;
5665+  height: 100%;
5666+  background: rgba(0, 0, 0, 0.2);
5667+  overflow: hidden;
5668+  position: fixed;
5669+  top: 0;
5670+  left: 0;
5671+  z-index: 2;
5672+}
5673+
5674+.dialog {
5675+  margin: 50px auto 0 auto;
5676+  background: #fff;
5677+  border: 1px solid #ddd;
5678
5679+  -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
5680+  -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
5681+  -o-box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
5682+  box-shadow: rgba(0, 0, 0, 0.4) 0 10px 40px;
5683+}
5684+
5685+.dialog_title {
5686+  margin: 0;
5687+  padding: 5px;
5688+  text-indent: 10px;
5689+  font-size: 1.3em;
5690+  color: #fff;
5691+  background: #2f343e;
5692+  border-bottom: 1px solid #1e2128;
5693+  text-shadow: #1e2128 0 1px 0;
5694+}
5695+
5696+#loader {
5697+  font-size: 2em;
5698+  width: 100%;
5699+  height: 100%;
5700+  text-align: center;
5701+  padding: 50px 0 0 0;
5702+}
5703+
5704+/*** Navigation columns ***/
5705+.column_container {
5706+  float: left;
5707+  min-width: 200px;
5708+  margin-right: 1px;
5709+}
5710+
5711+.column_container .column_header {
5712+  display: block;
5713+  width: inherit;
5714+  text-align: center;
5715+  font-size: 1.2em;
5716+  cursor: default;
5717+  padding: 2px 0;
5718+  background: #2f343e;
5719+  color: #fff;
5720+  text-shadow: #1e2128 0 1px 0;
5721+  border-right: 1px solid #1e2128;
5722+  border-bottom: 1px solid #1e2128;
5723+}
5724+
5725+.column_container .column_header span {
5726+  display: block;
5727+  vertical-align: middle;
5728+  text-overflow: ellipsis;
5729+  width: 100%;
5730+}
5731+
5732+.column_container .column_header:active, .column_container .column_header:focus, .column_header.active {
5733+  background-color: #1e2128;
5734+  padding: 3px 0 1px 0;
5735+  outline: 0;
5736+}
5737+
5738+.column_header.active {
5739+
5740+}
5741+
5742+.column_container .navigation_column {
5743+  border-right: 1px solid #ddd;
5744+}
5745+
5746+.column_container .navigation_column:last {
5747+  border-right: 5px solid #ddd;
5748+}
5749+
5750+.navigation_column {
5751+  width: 100%;
5752+  background: #fff url(../images/column_background.png) 0 0 repeat;
5753+/*  background-attachment: fixed; */
5754+  z-index: 1;
5755+}
5756+
5757+.navigation_column .column_items_box {
5758+  width: inherit;
5759+}
5760+
5761+.navigation_column .column_item {
5762+  display: block;
5763+  height: 20px;
5764+  padding: 5px 0;
5765+  width: inherit;
5766+  overflow: hidden;
5767+  text-overflow: ellipsis;
5768+  text-indent: 5px;
5769+  white-space: nowrap;
5770+}
5771+
5772+.navigation_column a.column_item {
5773+  display: block;
5774+  cursor: default;
5775+}
5776+
5777+.navigation_column .column_item img {
5778+  display: none;
5779+}
5780+
5781+.navigation_column .column_item span {
5782+  /*display: block;*/
5783+  vertical-align: middle;
5784+}
5785+
5786+.navigation_column .column_item span.subtitle {
5787+  opacity: 0.5;
5788+  font-size: 0.9em;
5789+  margin-left: 5px;
5790+  vertical-align: bottom;
5791+}
5792+
5793+.navigation_column .column_item_with_icon span {
5794+  margin-left: 20px;
5795+}
5796+
5797+.navigation_column .active_column_item, .menu_item:hover, .navigation_column .column_item:focus, .menu_item a:focus {
5798+  background: #33519d !important;
5799+  text-shadow: #000 0 1px 0;
5800+  color: #fff !important;
5801+  outline: 0 !important;
5802+}
5803+
5804+/*** Menus ***/
5805+.menu {
5806+  display: block;
5807+  text-indent: 0;
5808+  margin: 0 0 0 -1px;
5809+  padding: 3px 0;
5810+  position: fixed;
5811+  background: #fff;
5812+  color: #000;
5813+  min-width: 100px;
5814+  overflow: hidden;
5815+  text-overflow: ellipsis;
5816+  white-space: nowrap;
5817+  list-style: none;
5818+  cursor: default;
5819+  z-index: 5;
5820+  border: 1px solid #ddd;
5821
5822+  -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5823+  -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5824+  -o-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5825+  box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5826
5827+  -webkit-border-radius: 3px;
5828+  -moz-border-radius: 3px;
5829+  -o-border-radius: 3px;
5830+  border-radius: 3px;
5831+}
5832+
5833+.menu_item {
5834+  margin: 0;
5835+}
5836+
5837+.menu_item a {
5838+  display: block;
5839+  padding: 2px 0;
5840+  text-indent: 15px;
5841+  color: inherit;
5842+  text-decoration: none;
5843+  cursor: default;
5844+}
5845+
5846+.menu_item .menu_separator {
5847+  margin: 2px auto;
5848+  background: #fff !important;
5849+  padding: 0;
5850+  height: 1px;
5851+}
5852+
5853+.menu_item hr {
5854+  margin: auto;
5855+  padding: 0;
5856+  height: 1px;
5857+  color: #ddd;
5858+  width: 95%;
5859+}
5860+
5861+.menu_item.checked a:before {
5862+  content: " ✔ ";
5863+}
5864+
5865+.navigation_menu {
5866+  border-top: 0;
5867+  -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5868+  -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5869+  -o-box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5870+  box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px;
5871+}
5872+
5873+/*** Settings ***/
5874+#settings {
5875+  width: 600px;
5876+  height: 300px;
5877+}
5878+
5879+#settings .navigation_column {
5880+  border-right: 1px solid #c0c0c0;
5881+  width: 150px;
5882+  float: left;
5883+}
5884+
5885+#settings_controls {
5886+  width: 449px;
5887+  height: inherit;
5888+  float: right;
5889+  background: #f3f3f3;
5890+}
5891+
5892+#settings_controls .message {
5893+  text-align: center;
5894+  font-size: 2em;
5895+  color: #ddd;
5896+  margin-top: 70px;
5897+}
5898+
5899+#settings_controls .settings_header {
5900+  padding: 10px;
5901+  border-bottom: 1px solid #c0c0c0;
5902+  text-shadow: #fff 0 1px 0;
5903+  margin: 0;
5904+}
5905+
5906+#settings_controls .settings_header a {
5907+  color: #00f;
5908+  text-decoration: underline;
5909+}
5910+
5911+#settings_controls form {
5912+  background: #fff;
5913+  padding: 20px 0;
5914+}
5915+
5916+#settings_controls .setting_box {
5917+  padding: 2px 10px;
5918+  width: inherit;
5919+}
5920+
5921+#settings_controls .setting_box label {
5922+  width: 150px;
5923+  text-align: right;
5924+  display: inline-block;
5925+}
5926+
5927+#settings_controls .setting_box label.no_indent {
5928+  width: auto;
5929+  text-align: left;
5930+}
5931+
5932+#settings_controls .settings_footer {
5933+  border-top: 1px solid #c0c0c0;
5934+  text-align: right;
5935+  padding: 5px;
5936+}
5937+
5938+
5939+#save_settings {
5940+  font-weight: bold;
5941+  padding-top: 4px;
5942+  padding-bottom: 4px;
5943+}
5944+
5945+#revert_settings {
5946+  float: left;
5947+  background: transparent;
5948+  border-bottom: 1px transparent;
5949+}
5950adddir ./contrib/musicplayer/src/resources/images
5951addfile ./contrib/musicplayer/src/resources/images/column_background.png
5952binary ./contrib/musicplayer/src/resources/images/column_background.png
5953oldhex
5954*
5955newhex
5956*89504e470d0a1a0a0000000d49484452000000010000003c0802000000289347ad0000033b6943
5957*43504943432050726f66696c650000780185944b68d4501486ff8c2982b482a8b51694e0428bb4
5958*253ed08a50db69b5d6917118fbd022c83473671a4d333199191f884841dcf95a8a1b1f888b2ae2
5959*42ba5070a50b9142eb6b510471a52288423752c6ff26ed4c2a562f24f972ce7fcfeb8600550f53
5960*8e63453460d8cebbc9aea876e8f080b6780255a84135b85286e7b42712fb7da6563ee7afe9b750
5961*a465b249c68af51d98f8b46df5fd4b8f62efeb9ef6fa9ef9fa796f352e13028a46eb8a6cc05b25
5962*0f06bc57f2c9bc93a7e68864632895263be446b727d941be415e9a0df16088d3c23380aa366a72
5963*86e3324e6405b9a5686465cc51b26ea74d9b3c25ed69cf18a686fd467ec859d0c6958f01ad6b80
5964*452f2bb6010f18bd0bac5a5fb135d4012bfb81b12d15dbcfa43f1fa576dccb6cd9ec8753aaa3ac
5965*e943a9f4733db0f81a3073b554fa75ab549ab9cd1cace3996514dca2af6561ca2b20a837d8cdf8
5966*c9395a88839e7c550b709373ec5f02c42e00d73f021b1e00cb1f03891aa0670722e7d96e70e5c5
5967*29ce05e8c839a75d333b94d736ebfa76ad9d472bb46edb686ed45296a5f92e4f738527dca24837
5968*63d82a70cefe5ac67bb5b07b0ff2c9fe22e784b77b96959174aa93b34433dbfa92169dbbc98de4
5969*7b19734f37b981d754c6ddd31bb0b2d1cc77f7041ce9b3ad38cf456a2275f660fc0099f115d5c9
5970*47e53c248f78c58332a66f3f96da9720d7d39e3c9e8b494d2df7b69d19eae1194956ac33431df1
5971*597eed169232ef3a6aee3896ffcdb3b6c8731c8605011336ef363424d185289ae0c0450e197a4c
5972*2a4c5aa55fd06ac2c3f1bf2a2d24cabb2c2abaf0997b3efb7b4ea0c0dd327e1fa2718c34962368
5973*fa3bfd9bfe46bfa9dfd1bf5ea92f34543c23ee51d318bffc9d716566598d8c1bd428e3cb9a82f8
5974*06ab6da7d74296d6615e414f5e59df14ae2e635fa92f7b3499435c8c4f87ba14a14c4d18643cd9
5975*b5ecbe48b6f826fc7c73d9169a1eb52fce3ea9abe47aa38e1d99ac7e71365c0d6bffb3ab60d2b2
5976*abf0e48d902e3c6ba1ae5537a9dd6a8bba039aba4b6d535bd54ebeed54f79777f47256264eb26e
5977*97d5a7d8838dd3f4564eba325b04ff167e31fc2f75095bb8a6a1c97f68c2cd654c4bf88ee0f61f
5978*7748f92ffc0d0185150d7c4b3b3b000000097048597300000b1300000b1301009a9c180000001a
5979*49444154081d63f8ffff3f13030303ddf1dbafffe86e27d09f0052e10654d7b720ec0000000049
5980*454e44ae426082
5981adddir ./contrib/musicplayer/src/workers
5982addfile ./contrib/musicplayer/src/workers/indexer.js
5983hunk ./contrib/musicplayer/src/workers/indexer.js 1
5984+/**
5985+ *  == Workers ==
5986+ * 
5987+ *  Web Workers used to dispach computation-heavy work into background.
5988+ **/
5989+
5990+/** section: Workers, related to: CollectionScanner
5991+ * Indexer
5992+ *
5993+ *  This Worker is responsible for fetching MP3 files and then
5994+ *  extracting ID3 metadata, which could grately slowup the interface.
5995+ *
5996+ *  Messages sent to this worker have to contain only a read-cap to
5997+ *  an MP3 file stored in Tahoe (without /uri/ prefix).
5998+ *
5999+ *  Messages sent from this worker are objects returned by ID3 parser.
6000+ * 
6001+ **/
6002+
6003+var window = this,
6004+    document = {},
6005+    queue = 0;
6006+
6007+this.da = {};
6008+importScripts("env.js");
6009+
6010+/**
6011+ *  Indexer.onMessage(event) -> undefined
6012+ *  - event (Event): DOM event.
6013+ *  - event.data (String): Tahoe URI cap for an file.
6014+ * 
6015+ *  When tags are parsed, `postMessage` is called.
6016+ **/
6017+onmessage = function (event) {
6018+  var cap = event.data,
6019+      uri = "/uri/" + encodeURIComponent(cap);
6020
6021+  queue++;
6022+  new da.util.ID3({
6023+    url: uri,
6024+    onSuccess: function (tags) {
6025+      // To avoid duplication, we're using id property (which is mandatary) to store
6026+      // read-cap, which is probably already "more unique" than Math.uuid()
6027+      if(tags && typeof tags.title !== "undefined" && typeof tags.artist !== "undefined") {
6028+        tags.id = cap;
6029+        postMessage(tags);
6030+      }
6031+
6032+      // Not all files are reporeted instantly so it might
6033+      // take some time for scanner.js/CollectionScanner.js to
6034+      // report the files, maximum delay we're allowing here
6035+      // for new files to come in is one minute.
6036+      if(!--queue)
6037+        setTimeout(checkQueue, 1000*60*1);
6038+    },
6039+    onFailure: function () {
6040+      if(!--queue)
6041+        setTimeout(checkQueue, 1000*60*1);
6042+    }
6043+  });
6044+};
6045+
6046+function checkQueue() {
6047+  if(!queue)
6048+    postMessage("**FINISHED**");
6049+}
6050+
6051addfile ./contrib/musicplayer/src/workers/scanner.js
6052hunk ./contrib/musicplayer/src/workers/scanner.js 1
6053+/** section: Workers
6054+ * Scanner
6055+ * 
6056+ *  Scanner worker recursively scans the given root direcory for any type of files.
6057+ *  Messages sent to this worker should contain a directory cap (without `/uri/` part).
6058+ *  Messages sent from this worker are strings with read-only caps for each found file.
6059+ **/
6060+
6061+var window = this,
6062+    document = {},
6063+    queue = 0;
6064+
6065+this.da = {};
6066+importScripts("env.js");
6067+
6068+/**
6069+ *  Scanner.scan(object) -> undefined
6070+ *  - object (TahoeObject): an Tahoe object.
6071+ * 
6072+ *  Traverses the `object` until it finds a file, whose cap is then reported to main thread via `postMessage`.
6073+ **/
6074+function scan (obj) {
6075+  queue++;
6076+  obj.get(function () {
6077+    queue--;
6078+   
6079+    if(obj.type === "filenode")
6080+      return postMessage(obj.uri);
6081+   
6082+    var n = obj.children.length;
6083+    while(n--) {
6084+      var child = obj.children[n];
6085+     
6086+      if(child.type === "filenode")
6087+        postMessage(child.ro_uri);
6088+      else
6089+        scan(child);
6090+    }
6091+   
6092+    if(!queue)
6093+      postMessage("**FINISHED**");
6094+  });
6095+}
6096+
6097+/**
6098+ *  Scanner.onmessage(event) -> undefined
6099+ *  - event.data (String): Tahoe cap pointing to root directory from which scanning should begin.
6100+ **/
6101+onmessage = function (event) {
6102+  scan(new TahoeObject(event.data));
6103+};
6104adddir ./contrib/musicplayer/tests
6105adddir ./contrib/musicplayer/tests/data
6106addfile ./contrib/musicplayer/tests/data/songs.js
6107hunk ./contrib/musicplayer/tests/data/songs.js 1
6108+SHARED.songs = {
6109+  // ID3 v2.2 tag with UTF data
6110+  v22: {
6111+    data: "ID3%02%00%00%00%01I6TT2%00%00%11%01%EF%9F%BF%EF%9F%BEL%00j%00%EF%9F%B3%00s%00i%00%EF%9F%B0%00%00%00TP1%00%00!%01%EF%9F%BF%EF%9F%BE%EF%9F%93%00l%00a%00f%00u%00r%00%20%00A%00r%00n%00a%00l%00d%00s%00%00%00TP2%00%00!%01%EF%9F%BF%EF%9F%BE%EF%9F%93%00l%00a%00f%00u%00r%00%20%00A%00r%00n%00a%00l%00d%00s%00%00%00TCM%00%00!%01%EF%9F%BF%EF%9F%BE%EF%9F%93%00l%00a%00f%00u%00r%00%20%00A%00r%00n%00a%00l%00d%00s%00%00%00TAL%00%00%0D%00Found%20Songs%00TRK%00%00%05%007%2F7%00TYE%00%00%06%002009%00COM%00%00%10%00engiTunPGAP%000%00%00TEN%00%00%0E%00iTunes%208.0.2%00COM%00%00h%00engiTunNORM%00%20000007AA%2000000B2E%2000006443%200000967A%200000BF53%2000016300%200000821A%200000816B%2000010C29%20000166FA%00COM%00%00%EF%9E%82%00engiTunSMPB%00%2000000000%2000000210%200000079B%2000000000008BDDD5%2000000000%20004C0FD7%2000000000%2000000000%2000000000%2000000000%2000000000%2000000000%00TPA%00%00%05%001%2F1%00TCO%00%00%0F%00Neo-Classical%00COM%00%00%22%00eng%00available%20on%20ErasedTapes.com>>>PADDING<<<%EF%9F%BF",
6112+    simplified: {
6113+      title: "Lj\u00f3si\u00f0",
6114+      artist: "\u00d3lafur Arnalds",
6115+      album: "Found Songs",
6116+      track: 7,
6117+      year:  2009,
6118+      genre: 0,
6119+      lyrics: "",
6120+      links: {
6121+        official: ""
6122+      }
6123+    },
6124+    frames: {
6125+      TT2: "Lj\u00f3si\u00f0",
6126+      TP1: "\u00d3lafur Arnalds",
6127+      TP2: "\u00d3lafur Arnalds",
6128+      TAL: "Found Songs",
6129+      TRK: 7,
6130+      TYE: 2009
6131+    }
6132+  },
6133
6134+  // ID3 v2.3 tag
6135+  v23: {
6136+    data: "ID3%03%00%00%00%00Q%01TPOS%00%00%00%04%00%00%001%2F1TENC%00%00%00%0E%40%00%00iTunes%20v7.6.2TIT2%00%00%005%00%00%01%EF%9F%BF%EF%9F%BED%00e%00a%00t%00h%00%20%00W%00i%00l%00l%00%20%00N%00e%00v%00e%00r%00%20%00C%00o%00n%00q%00u%00e%00r%00%00%00TPE1%00%00%00%15%00%00%01%EF%9F%BF%EF%9F%BEC%00o%00l%00d%00p%00l%00a%00y%00%00%00TCON%00%00%00%0D%00%00%01%EF%9F%BF%EF%9F%BER%00o%00c%00k%00%00%00COMM%00%00%00h%00%00%00engiTunNORM%00%20000002F6%200000036E%2000001471%200000163D%2000000017%2000000017%20000069F3%2000006AA9%2000000017%2000000017%00RVAD%00%00%00%0A%00%00%03%105555>>>PADDING<<<%EF%9F%BF",
6137+    simplified: {
6138+      title: "Death Will Never Conquer",
6139+      artist: "Coldplay",
6140+      album: "Unknown",
6141+      track: 0,
6142+      year:  0,
6143+      genre: 0,
6144+      lyrics: "",
6145+      links: {
6146+        official: ""
6147+      }
6148+    },
6149+    frames: {
6150+      TIT2: "Death Will Never Conquer",
6151+      TPE1: "Coldplay",
6152+      TCON: 0
6153+    }
6154+  },
6155
6156+  // ID3 v2.4 tag
6157+  v24: {
6158+    data: "ID3%04%00%00%00%00%02%00TRCK%00%00%00%05%00%00%006%2F10TIT2%00%00%00%08%00%00%00HalcyonTPE1%00%00%00%08%00%00%00DelphicTALB%00%00%00%08%00%00%00AcolyteTYER%00%00%00%05%00%00%002010TCON%00%00%00%0F%00%00%00(52)ElectronicWXXX%00%00%00%13%00%00%00%00http%3A%2F%2Fdelphic.ccTPUB%00%00%00%13%00%00%00Chimeric%20%2F%20PolydorTPOS%00%00%00%04%00%00%001%2F1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%EF%9F%BF",
6159+    simplified: {
6160+      title: "Halcyon",
6161+      artist: "Delphic",
6162+      album: "Acolyte",
6163+      track: 6,
6164+      year: 2010,
6165+      genre: 52, // Electornic,
6166+      lyrics: "",
6167+      links: {
6168+        official: "http://delphic.cc"
6169+      }
6170+    },
6171+    frames: {
6172+      TIT2: "Halcyon",
6173+      TPE1: "Delphic",
6174+      TALB: "Acolyte",
6175+      TYER: 2010,
6176+      TCON: 52,
6177+      TRCK: 6,
6178+      WXXX: "http://delphic.cc"
6179+    }
6180+  },
6181
6182+  // ID3 v1 tag
6183+  v1: {
6184+    data: "TAGYeah%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00Queen%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00Made%20In%20Heaven%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%001995%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%0C%0C",
6185+    simplified: {
6186+      title:  "Yeah",
6187+      artist: "Queen",
6188+      album:  "Made In Heaven",
6189+      track:  12,
6190+      year:   1995,
6191+      genre:  12
6192+    }
6193+  },
6194
6195+  // 1x1 transparent PNG file
6196+  image: {
6197+    data: "%EF%9E%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%01%00%00%00%01%01%03%00%00%00%25%EF%9F%9BV%EF%9F%8A%00%00%00%03PLTE%00%00%00%EF%9E%A7z%3D%EF%9F%9A%00%00%00%01tRNS%00%40%EF%9F%A6%EF%9F%98f%00%00%00%0AIDAT%08%EF%9F%97c%60%00%00%00%02%00%01%EF%9F%A2!%EF%9E%BC3%00%00%00%00IEND%EF%9E%AEB%60%EF%9E%82"
6198+  }
6199+};
6200+
6201+(function (args) {
6202+  // SONGS.v22 and SONGS.v23 have vast amount of padding bits,
6203+  // so we're adding them programatically
6204
6205+  function addPaddingTo(key, n) {
6206+    var p = [];
6207+    while(n--)
6208+      p.push("%00");
6209+   
6210+    SHARED.songs[key].data = SHARED.songs[key].data.replace(">>>PADDING<<<", p.join(""));
6211+  }
6212
6213+  addPaddingTo("v22", 25241);
6214+  addPaddingTo("v23", 10084);
6215+})();
6216+
6217addfile ./contrib/musicplayer/tests/initialize.js
6218hunk ./contrib/musicplayer/tests/initialize.js 1
6219+windmill.jsTest.require("shared.js");
6220+
6221+windmill.jsTest.register([
6222+//  'test_utils',
6223+  'test_Goal',
6224+  'test_BinaryFile',
6225+  'test_ID3',
6226+  'test_ID3v1',
6227+  'test_ID3v2',
6228+  'test_BrowserCouch',
6229+  'test_DocumentTemplate',
6230
6231+  'test_NavigationController'
6232+]);
6233addfile ./contrib/musicplayer/tests/shared.js
6234hunk ./contrib/musicplayer/tests/shared.js 1
6235+var SHARED = {};
6236+var util = {
6237+  wait_for_data: function (key) {
6238+    return {
6239+      method: 'waits.forJS',
6240+      params: {
6241+        js: function () { return !!SHARED[key]; }
6242+      }
6243+    }
6244+  },
6245
6246+  create_id3v2_test: function (version, size) {
6247+    var ID3v2Parser = da.util.ID3v2Parser,
6248+        vkey = "v" + (version * 10);
6249+   
6250+    return new function () {
6251+      var self = this;
6252+     
6253+      this.setup = function () {
6254+        self.simplified = null;
6255+        self.frames     = null;
6256hunk ./contrib/musicplayer/tests/shared.js 23
6257+        var data = da.util.BinaryFile.fromEncodedString(SHARED.songs[vkey].data);
6258+        self.parser = new ID3v2Parser(data, {
6259+          url: "/fake/" + Math.uuid(),
6260+          onSuccess: function (simplified, frames) {
6261+            self.simplified = simplified;
6262+            self.frames = frames;
6263+          }
6264+        }, {});
6265+      };
6266+
6267+      this.test_waitForData = {
6268+        method: 'waits.forJS',
6269+        params: {
6270+          js: function () { return !!self.simplified && !!self.frames; }
6271+        }
6272+      };
6273+     
6274+      this.test_header = function () {
6275+        jum.assertEquals("version should be " + version,  self.parser.version, version);
6276+        jum.assertEquals("no flags should be set",        self.parser.header.flags, 0);
6277+        jum.assertEquals("tag size shoudl be " + size,    self.parser.header.size,  size);
6278+      };
6279+
6280+      this.test_verifySimplifiedResult = function () {
6281+        jum.assertSameObjects(SHARED.songs[vkey].simplified, self.simplified);
6282+      };
6283+
6284+      this.test_verifyDetectedFrames = function () {
6285+        jum.assertSameObjects(SHARED.songs[vkey].frames,self.frames);
6286+      };
6287+     
6288+      return this;
6289+    };
6290+  }
6291+};
6292+
6293+jum.assertSameObjects = function (a, b) {
6294+  if(a === b)
6295+    return true;
6296+  // catches cases when one of args is null
6297+  if(!a || !b)
6298+    jum.assertEquals(a, b);
6299
6300+  for(var prop in a)
6301+    if(a.hasOwnProperty(prop))
6302+      if(prop in a && prop in b)
6303+        if(typeof a[prop] === "object")
6304+          jum.assertSameObjects(a[prop], b[prop]);
6305+        else
6306+          jum.assertEquals(a[prop], b[prop]);
6307+      else
6308+        jum.assertTrue("missing '" + prop +"' property", false);
6309
6310+  return true;
6311+};
6312addfile ./contrib/musicplayer/tests/test_BinaryFile.js
6313hunk ./contrib/musicplayer/tests/test_BinaryFile.js 1
6314+windmill.jsTest.require("data/");
6315+
6316+var test_BinaryFile = new function () {
6317+  var BinaryFile = da.util.BinaryFile,
6318+      self = this;
6319
6320+  this.setup = function () {
6321+    this.file_le = new BinaryFile("\0\0\1\0");
6322+    this.file_be = new BinaryFile("\0\1\0\0", {bigEndian: true});   
6323+    this.bond = new BinaryFile("A\0\0\7James Bond\0");
6324+  };
6325
6326+  this.test_options = function () {
6327+    jum.assertEquals(4, this.file_le.length);
6328+    jum.assertFalse(this.file_le.bigEndian);
6329+   
6330+    jum.assertEquals(4, this.file_be.length);
6331+    jum.assertTrue(this.file_be.bigEndian);
6332+  };
6333
6334+  this.test_getByte = function () {
6335+    jum.assertEquals(0, this.file_le.getByteAt(0));
6336+    jum.assertEquals(1, this.file_le.getByteAt(2));
6337+   
6338+    jum.assertEquals(0, this.file_be.getByteAt(0));
6339+    jum.assertEquals(1, this.file_be.getByteAt(1));
6340+  };
6341
6342+  this.test_getShort = function () {
6343+    jum.assertEquals(0,   this.file_le.getShortAt(0)); // 00
6344+    jum.assertEquals(256, this.file_le.getShortAt(1)); // 01
6345+    jum.assertEquals(1,   this.file_le.getShortAt(2)); // 10
6346+   
6347+    jum.assertEquals(1,   this.file_be.getShortAt(0)); // 01
6348+    jum.assertEquals(256, this.file_be.getShortAt(1)); // 10
6349+    jum.assertEquals(0,   this.file_be.getShortAt(2)); // 00
6350+  };
6351
6352+  this.test_getLong = function () {
6353+    jum.assertEquals(65536, this.file_le.getLongAt(0));
6354+    jum.assertEquals(65536, this.file_be.getLongAt(0));
6355+  };
6356
6357+  this.test_getBits = function () {
6358+    jum.assertSameObjects([0, 1], this.file_le.getBitsAt(2, 2));
6359+    jum.assertSameObjects([0, 0, 0, 1], this.file_be.getBitsAt(1, 4));
6360+  };
6361
6362+  this.test_unpack = function () {
6363+    jum.assertSameObjects(["A", 0, 0, 7], this.bond.unpack("c3i"));
6364+    jum.assertSameObjects(["James Bond"], this.bond.unpack("4x10S"));
6365+  };
6366
6367+  this.test_toEncodedString = function () {
6368+    jum.assertEquals("%00%00%01%00", this.file_le.toEncodedString());
6369+    jum.assertEquals("%00%01%00%00", this.file_be.toEncodedString());
6370+  };
6371
6372+  return this;
6373+};
6374addfile ./contrib/musicplayer/tests/test_BrowserCouch.js
6375hunk ./contrib/musicplayer/tests/test_BrowserCouch.js 1
6376+windmill.jsTest.require("shared.js");
6377+
6378+var test_BrowserCouchDict = new function () {
6379+  var BrowserCouch = da.db.BrowserCouch,
6380+      self = this;
6381
6382+  this.setup = function () {
6383+    self.dict = new BrowserCouch.Dictionary();
6384+   
6385+    this.dict.set("a", 1);
6386+    this.dict.set("b", 2);
6387+   
6388+    this.dict.setDocs([
6389+      {id: "c", value: 3},
6390+      {id: "d", value: 4},
6391+      {id: "a", value: 5}
6392+    ]);
6393+  };
6394
6395+  this.test_set = function () {
6396+    jum.assertTrue(this.dict.has("a"));
6397+    jum.assertTrue(this.dict.has("b"));
6398+    jum.assertFalse(this.dict.has("x"));
6399+   
6400+    jum.assertSameObjects({id:"a", value: 5}, this.dict.dict.a);
6401+    jum.assertEquals(2, this.dict.dict.b);
6402+  };
6403
6404+  this.test_setDocs = function () {
6405+    jum.assertTrue(this.dict.has("c"));
6406+    jum.assertTrue(this.dict.has("d"));
6407+   
6408+    jum.assertEquals(3, this.dict.dict.c.value);
6409+    jum.assertEquals(4, this.dict.dict.d.value);
6410+  };
6411
6412+  this.test_remove = function () {
6413+    this.dict.remove("a");
6414+    jum.assertEquals(3, this.dict.keys.length);
6415+    jum.assertFalse(this.dict.has("a"));   
6416+  };
6417
6418+  this.test_unpickle = function () {
6419+    this.dict.unpickle({
6420+      x: 2.2,
6421+      y: 2.3
6422+    });
6423+   
6424+    jum.assertEquals(2, this.dict.keys.length);
6425+    jum.assertTrue(this.dict.has("x"));
6426+    jum.assertTrue(this.dict.has("y"));
6427+    jum.assertFalse(this.dict.has("a"));
6428+  };
6429
6430+  this.test_clear = function () {
6431+    this.dict.clear();
6432+   
6433+    jum.assertEquals(0, this.dict.keys.length);
6434+    jum.assertFalse(this.dict.has("x"));
6435+    jum.assertFalse(this.dict.has("b"));
6436+  };
6437+};
6438+
6439+var test_BrowserCouch = new function () {
6440+  var BrowserCouch = da.db.BrowserCouch,
6441+      self = this;
6442
6443+  this.setup = function () {
6444+    this.db = false;
6445+    this.stored = {};
6446+   
6447+    BrowserCouch.get("test1", function (db) {
6448+      self.db = db;
6449+      db.addEvent("store", function (doc) {
6450+        self.stored[doc.id] = new Date();
6451+      });
6452+    });
6453+  };
6454
6455+  this.test_waitForDb = {
6456+    method: 'waits.forJS',
6457+    params: {
6458+      js: function () { return !!self.db; }
6459+    }
6460+  };
6461
6462+  this.test_verifyDb = function () {
6463+    jum.assertEquals(0, this.db.getLength());
6464+  };
6465
6466+  this.test_put = function () {
6467+    var cb = {doc1: 0, doc2: 0, doc3: 0};
6468+    this.db.put({id: "doc1", test: 1}, function () { cb.doc1++ });
6469+    this.db.put({id: "doc2", test: 2}, function () { cb.doc2++ });
6470+    this.db.put({id: "doc3", test: 3}, function () { cb.doc3++ });
6471+    this.db.put({id: "doc1", test: 4}, function () { cb.doc1++ });
6472+   
6473+    jum.assertEquals(2, cb.doc1);
6474+    jum.assertEquals(1, cb.doc2);
6475+    jum.assertEquals(1, cb.doc3);
6476+  };
6477
6478+  this.test_storeEvent = function () {
6479+    jum.assertTrue(self.stored.doc1 >= self.stored.doc3);
6480+    jum.assertTrue(self.stored.doc3 >= self.stored.doc2);
6481+  };
6482
6483+  this.test_wipe = function () {
6484+    jum.assertEquals(3, this.db.getLength());
6485+    this.db.wipe();
6486+   
6487+    BrowserCouch.get("test1", function (db) {
6488+      jum.assertEquals(0, db.getLength());
6489+    });
6490+  };
6491
6492+  this.teardown = function () {
6493+    self.db.wipe();   
6494+  };
6495
6496+  return this;
6497+};
6498+
6499+var test_BrowserCouch_tempView = new function () {
6500+  var BrowserCouch = da.db.BrowserCouch,
6501+      self = this;
6502
6503+  this.setup = function () {
6504+    BrowserCouch.get("test2", function (db) {
6505+      self.db = db;
6506+      self.map_called = 0;
6507+      self.map_updated_called = false;
6508+      self.reduce_updated_called = false;
6509+     
6510+      db.put([
6511+        {id: "doc1", nr: 1},
6512+        {id: "doc2", nr: 2},
6513+        {id: "doc3", nr: 3}
6514+      ], function () {
6515+        self.docs_saved = true;
6516+      });
6517+    });
6518+  };
6519
6520+  this.test_waitForDb = {
6521+    method: 'waits.forJS',
6522+    params: {
6523+      js: function () { return !!self.db && self.docs_saved; }
6524+    }
6525+  };
6526
6527+  this.test_map = function () {
6528+    this.db.view({
6529+      temporary: true,
6530+     
6531+      map: function (doc, emit) {
6532+        self.map_called++;
6533+        if(doc.nr !== 2)
6534+          emit(doc.id, doc.nr);
6535+      },
6536+     
6537+      finished: function (result) {
6538+        self.map_result = result;
6539+       
6540+        self.db.put({id: "doc4", nr: 4});
6541+      },
6542+     
6543+      updated: function () {
6544+        self.map_updated_called = true;
6545+      }
6546+    })
6547+  };
6548
6549+  this.test_waitForMapResult = {
6550+    method: 'waits.forJS',
6551+    params: {
6552+      js: function () { return !!self.map_result }
6553+    }
6554+  };
6555
6556+  this.test_verifyMapResult = function () {
6557+    var mr = self.map_result;
6558+   
6559+    jum.assertEquals(3, self.map_called);
6560+    jum.assertTrue("rows" in mr);
6561+    jum.assertEquals(2, mr.rows.length);
6562+    jum.assertEquals("function", typeof mr.findRow);
6563+    jum.assertEquals("function", typeof mr.getRow);
6564+    jum.assertFalse(self.map_updated_called);
6565+  };
6566
6567+  this.test_mapFindRow = function () {
6568+    var mr = self.map_result;
6569+    jum.assertEquals(-1, mr.findRow("doc2"));
6570+    jum.assertEquals(-1, mr.findRow("doc4"));
6571+    jum.assertEquals(-1, mr.findRow("doc7"));
6572+    jum.assertEquals(0,  mr.findRow("doc1"));
6573+  };
6574
6575+  this.test_reduce = function () {
6576+    self.reduce_called = 0;
6577+    self.db.view({
6578+      temporary: true,
6579+     
6580+      map: function (doc, emit) {
6581+        emit(doc.nr%2 ? "odd" : "even", doc.nr);
6582+      },
6583+     
6584+      reduce: function (keys, values) {
6585+        var sum = 0, n = values.length;
6586+        self.reduce_called++;
6587+       
6588+        while(n--)
6589+          sum += values[n];
6590+       
6591+        return sum;
6592+      },
6593+     
6594+      finished: function (result) {
6595+        self.reduce_result = result;
6596+        self.db.put({id: "doc5", nr: 5});
6597+      },
6598+     
6599+      updated: function () {
6600+        self.reduce_updated_called = true;
6601+      }
6602+    });
6603+  };
6604
6605+  this.test_waitForReduceResult = {
6606+    method: 'waits.forJS',
6607+    params: {
6608+      js: function () { return !!self.reduce_result }
6609+    }
6610+  };
6611
6612+  this.test_verifyReduceResult = function () {
6613+    var rr = this.reduce_result;
6614+    jum.assertFalse(this.reduce_updated_called);
6615+   
6616+    jum.assertEquals("function", typeof rr.findRow);
6617+    jum.assertEquals("function", typeof rr.getRow);
6618+   
6619+    jum.assertEquals(2, self.reduce_called);
6620+    jum.assertEquals(2, rr.rows.length);
6621+  };
6622
6623+  this.test_verifyReduceFindRow = function () {
6624+    var rr = this.reduce_result;
6625+   
6626+    jum.assertTrue(rr.findRow("even") !== -1);
6627+    jum.assertTrue(rr.findRow("odd")  !== -1);
6628+    jum.assertEquals(-1, rr.findRow("even/odd"));
6629+   
6630+    jum.assertEquals(6, rr.getRow("even")); // 2 + 4
6631+    jum.assertEquals(4, rr.getRow("odd"));  // 1 + 3
6632+  };
6633
6634+  this.teardown = function () {
6635+    this.db.wipe();
6636+  };
6637
6638+  return this;
6639+};
6640+
6641+var test_BrowserCouch_liveView = new function () {
6642+  var BrowserCouch = da.db.BrowserCouch,
6643+      self = this;
6644
6645+  this.setup = function () {
6646+    this.docs_saved = false;
6647+   
6648+    this.map_result = null;
6649+    this.map_updated = null;
6650+    this.map_finished_called = 0;
6651+    this.map_updated_called = 0;
6652+   
6653+    this.reduce_result = null;
6654+    this.reduce_updated = null;
6655+    this.reduce_finished_called = 0;
6656+    this.reduce_updated_called = 0;
6657+   
6658+    BrowserCouch.get("test3", function (db) {
6659+      self.db = db;
6660+     
6661+      db.put([
6662+        {id: "Keane",           albums: 3, formed: 1997},
6663+        {id: "Delphic",         albums: 1, formed: 2010},
6664+        {id: "The Blue Nile",   albums: 4, formed: 1981}
6665+      ], function () {
6666+        self.docs_saved = true;
6667+       
6668+        db.view({
6669+          id: "test1",
6670+         
6671+          map: function (doc, emit) {
6672+            if(doc.id.toLowerCase().indexOf("the") === -1)
6673+              emit(doc.id, doc.formed);
6674+          },
6675+         
6676+          finished: function (view) {
6677+            self.map_finished_called++;
6678+            self.map_result = view;
6679+          },
6680+         
6681+          updated: function (view) {
6682+            self.map_updated_called++;
6683+            self.map_updates = view;
6684+          }
6685+        });
6686+      });
6687+    });
6688+  };
6689
6690+  this.test_waitForDb = {
6691+    method: 'waits.forJS',
6692+    params: {
6693+      js: function () { return !!self.db && self.docs_saved && !!self.map_result }
6694+    }
6695+  };
6696
6697+  this.test_verifyMap = function () {
6698+    var mr = self.map_result;
6699+   
6700+    jum.assertEquals(1, self.map_finished_called);
6701+    jum.assertEquals(2, mr.rows.length);
6702+    jum.assertEquals("function", typeof mr.findRow);
6703+   
6704+    jum.assertEquals(-1, mr.findRow("The Drums"));
6705+    jum.assertEquals(-1, mr.findRow("The Blue Nile"));
6706+    jum.assertEquals(0,  mr.findRow("Delphic"));
6707+   
6708+    self.db.put([
6709+      {id: "Marina and The Diamonds", albums: 1, formed: 2007},
6710+      {id: "Coldplay",                albums: 4, formed: 1997},
6711+      {id: "Delphic",                 albums: 1, formed: 2009}
6712+    ], function () {
6713+      self.map_updates_saved = true;
6714+    });
6715+  };
6716
6717+  this.test_waitForUpdate = {
6718+    method: 'waits.forJS',
6719+    params: {
6720+      js: function () { return self.map_updates_saved && !!self.map_updates }
6721+    }
6722+  };
6723
6724+  this.test_verifyMapUpdates = function () {
6725+    var mr = self.map_result,
6726+        mu = self.map_updates;
6727+   
6728+    jum.assertEquals(1, self.map_updated_called);
6729+    jum.assertEquals(1, self.map_finished_called);
6730+    jum.assertEquals(2, mu.rows.length);
6731+    jum.assertEquals(3, mr.rows.length);
6732+   
6733+    jum.assertEquals(-1, mu.findRow("Marina and The Diamonds"));
6734+    jum.assertEquals(-1, mu.findRow("Keane"));
6735+    jum.assertEquals(0,  mu.findRow("Coldplay"));
6736+   
6737+    jum.assertEquals(-1, mr.findRow("Marina and The Diamonds"));
6738+    jum.assertEquals(0,  mr.findRow("Coldplay"));
6739+   
6740+    jum.assertEquals(2009, mr.getRow("Delphic"));
6741+  };
6742
6743+  this.test_killView = function () {
6744+    self.db.killView("test1");
6745+    self.db.put({id: "Noisettes", formed: 2003, albums: 2}, $empty);
6746+  };
6747
6748+  this.test_waitForViewToDie = {
6749+    method: 'waits.forJS',
6750+    params: {
6751+      js: function () { return !!!self.db.views.test1 }
6752+    }
6753+  };
6754
6755+  this.test_viewIsDead = function () {
6756+    jum.assertEquals(1, self.map_updated_called);
6757+  };
6758
6759+  this.test_reduce = function () {
6760+    self.rereduce_args = null;
6761+    self.rereduce_called = 0;
6762+    self.reduce_called = 0;
6763+   
6764+    self.db.view({
6765+      id: "test2",
6766+     
6767+      map: function (doc, emit) {
6768+        if(doc.albums)
6769+          emit("albums", doc.albums);
6770+      },
6771+     
6772+      reduce: function (keys, values, rereduce) {
6773+        if(rereduce) {
6774+          self.rereduce_args = arguments;
6775+          self.rereduce_called++;
6776+        } else {
6777+          self.reduce_called++;
6778+        }
6779+       
6780+        var n = values.length, sum = 0;
6781+        while(n--) sum += values[n];
6782+        return sum;
6783+      },
6784+     
6785+      finished: function (view) {
6786+        self.reduce_finished_called++;
6787+        self.reduce_result = view;
6788+      },
6789+     
6790+      updated: function (view) {
6791+        self.reduce_updated_called++;
6792+        self.reduce_updates = view;
6793+      }
6794+    })
6795+  };
6796
6797+  this.test_waitForReduce = {
6798+    method: 'waits.forJS',
6799+    params: {
6800+      js: function () { return !!self.reduce_result }
6801+    }
6802+  };
6803
6804+  this.test_verifyReduceResult = function () {
6805+    var rr = self.reduce_result;
6806+   
6807+    jum.assertEquals(1, self.reduce_finished_called);
6808+    jum.assertEquals(0, self.reduce_updated_called);
6809+   
6810+    jum.assertEquals("function", typeof rr.findRow);
6811+   
6812+    jum.assertEquals(1,   rr.rows.length);
6813+    jum.assertEquals(0,   rr.findRow("albums"));
6814+    jum.assertEquals(15,  rr.getRow("albums"));
6815+   
6816+    self.db.put([
6817+      {id: "Imaginary", albums: 0, formed: 2020},
6818+      {id: "Grizzly Bear", albums: 2, formed: 2000}
6819+    ], function() {
6820+      self.reduce_updates_saved = true;
6821+    });
6822+  };
6823+   
6824+  this.test_waitForUpdates = {
6825+    method: 'waits.forJS',
6826+    params: {
6827+      js: function () {  return self.reduce_updates_saved }
6828+    }
6829+  };
6830
6831+  this.test_reduceUpdates = function () {
6832+    var rr = self.reduce_result,
6833+        ru = self.reduce_updates;
6834+   
6835+    jum.assertEquals(1, self.reduce_updated_called);
6836+    jum.assertEquals(1, self.reduce_finished_called);
6837+   
6838+    jum.assertEquals(1,   ru.rows.length);
6839+    jum.assertEquals(-1,  ru.findRow("Grizzly Bear"));
6840+    jum.assertEquals(0,   ru.findRow("albums"));
6841+   
6842+    jum.assertEquals(2,   ru.getRow("albums"));
6843+    jum.assertEquals(17,  rr.getRow("albums"));
6844+  };
6845
6846+  this.test_rereduce = function () {
6847+    jum.assertEquals(1, self.rereduce_called);
6848+    jum.assertSameObjects([null, [2, 15], true], self.rereduce_args);
6849+  }
6850
6851+  this.teardown = function () {
6852+    self.db.killView("test2");
6853+  };
6854
6855+  return this;
6856+};
6857addfile ./contrib/musicplayer/tests/test_DocumentTemplate.js
6858hunk ./contrib/musicplayer/tests/test_DocumentTemplate.js 1
6859+windmill.jsTest.require("shared.js");
6860+
6861+var test_DocumentTemplate = new function () {
6862+  var BrowserCouch = da.db.BrowserCouch,
6863+      DocumentTemplate = da.db.DocumentTemplate,
6864+      self = this;
6865+
6866+  this.setup = function () {
6867+    self.db = null;
6868+    BrowserCouch.get("dt_test1", function (db) {
6869+      self.db = db;
6870+    });
6871+  };
6872
6873+  this.waitForDb = {
6874+    method: "waits.forJS",
6875+    params: {
6876+      js: function () { return !!self.db }
6877+    }
6878+  };
6879
6880+  this.test_registerType = function () {
6881+    DocumentTemplate.registerType("test_Person", self.db, new Class({
6882+      Extends: DocumentTemplate,
6883+     
6884+      hasMany: {
6885+        cars: ["test_Car", "owner_id"]
6886+      },
6887+     
6888+      sayHi: function () {
6889+        return "Hello! My name is %0 %1.".interpolate([
6890+          this.get("name"),
6891+          this.get("surname")
6892+        ])
6893+      }
6894+    }));
6895+    self.Person = DocumentTemplate.test_Person;
6896+   
6897+    DocumentTemplate.registerType("test_Car", self.db, new Class({
6898+      Extends: DocumentTemplate,
6899+     
6900+      belongsTo: {
6901+        owner: "test_Person"
6902+      },
6903+     
6904+      start: function () {
6905+        this.update({state: "inMotion"})
6906+      },
6907+     
6908+      stop: function () {
6909+        this.update({state: "stopped"});
6910+      },
6911+     
6912+      isRunning: function () {
6913+        return this.get("state") === "inMotion"
6914+      }
6915+    }));
6916+    this.Car = DocumentTemplate.test_Car;
6917+   
6918+    jum.assertTrue("test_Person" in DocumentTemplate);
6919+    jum.assertTrue("test_Person" in self.db.views);
6920+    jum.assertEquals(self.db.name, self.Person.db().name);
6921+  };
6922
6923+  this.test_instanceFindNoResult = function () {
6924+    this.instanceFind_success_called = 0;
6925+    this.instanceFind_failure_called = 0;
6926+   
6927+    this.Car.find({
6928+      properties: {manufacturer: "Volkswagen"},
6929+      onSuccess: function () {
6930+        self.instanceFind_success_called++;
6931+      },
6932+      onFailure: function () {
6933+        self.instanceFind_failure_called++;
6934+      }
6935+    })
6936+  };
6937
6938+  this.test_waitForInstanceFind = {
6939+    method: "waits.forJS",
6940+    params: {
6941+      js: function () { return self.instanceFind_failure_called }
6942+    }
6943+  };
6944
6945+  this.test_verifyInstaceFind = function () {
6946+    jum.assertEquals(1, self.instanceFind_failure_called);
6947+    jum.assertEquals(0, self.instanceFind_success_called);
6948+  };
6949
6950+  this.test_createDoc = function () {
6951+    self.herbie_saved = 0;
6952+    self.Person.create({
6953+      id:     "jim",
6954+      first:  "Jim",
6955+      last:   "Douglas"
6956+    }, function (jim) {
6957+      self.jim = jim;
6958+     
6959+      self.herbie = new self.Car({
6960+        id:       "herbie",
6961+        owner_id: "jim",
6962+        state:    "sleeping",
6963+        diamods:  0
6964+      });
6965+     
6966+      self.herbie.save(function () {
6967+        self.herbie_saved++;
6968+      });
6969+    });
6970+  };
6971
6972+  this.test_waitForDocs = {
6973+    method: "waits.forJS",
6974+    params: {
6975+      js: function () { return !!self.jim && !!self.herbie && self.herbie_saved }
6976+    }
6977+  };
6978
6979+  this.test_verifyCreate = function () {
6980+    jum.assertEquals("jim",     self.jim.id);
6981+    jum.assertEquals("herbie",  self.herbie.id);
6982+    jum.assertEquals(1,         self.db.views.test_Person.view.rows.length);
6983+    jum.assertEquals(1,         self.db.views.test_Car.view.rows.length);
6984+  };
6985
6986+  this.test_get = function () {
6987+    jum.assertEquals("Jim", self.jim.get("first"));
6988+    jum.assertEquals("jim", self.herbie.get("owner_id"));   
6989+  };
6990
6991+  this.test_belongsTo = function () {
6992+    self.herbie.get("owner", function (owners) {
6993+      jum.assertEquals(1, owners.length);
6994+      jum.assertEquals(self.jim.id, owners[0].id);
6995+      self.got_jim = true;
6996+    });
6997+  };
6998
6999+  this.test_waitForJim = {
7000+    method: "waits.forJS",
7001+    params: {
7002+      js: function () { return self.got_jim }
7003+    }
7004+  };
7005
7006+  this.test_hasMany = function () {
7007+    self.jim.get("cars", function (cars) {
7008+      jum.assertEquals(1, cars.length);
7009+      jum.assertEquals(self.herbie.id, cars[0].id);
7010+      self.got_herbie = true;
7011+    });
7012+  };
7013
7014+  this.test_waitForHerbie = {
7015+    method: "waits.forJS",
7016+    params: {
7017+      js: function () { return self.got_herbie }
7018+    }
7019+  };
7020
7021+  this.test_propertyChangeEvent = function () {
7022+    self.herbie.addEvent("propertyChange", function (changes, herbie) {
7023+      jum.assertEquals(self.herbie, herbie);
7024+      jum.assertTrue("state" in changes);
7025+      jum.assertFalse("id" in changes);
7026+      jum.assertEquals("inMotion", herbie.get("state"));
7027+    });
7028+   
7029+    self.herbie.start();
7030+  };
7031
7032+  this.test_findOrCreate = function () {
7033+    self.foc_finished = 0;
7034+    self.Person.findOrCreate({
7035+      properties: {id: "jim"},
7036+      onSuccess: function (jim, created) {
7037+        self.foc_finished++;
7038+        self.foc_jim = {jim: jim, created: created};
7039+      }
7040+    });
7041+   
7042+    self.john_props = {id: "john", first: "John", last: "Doe"};
7043+    self.Person.findOrCreate({
7044+      properties: self.john_props,
7045+      onSuccess: function (john, created) {
7046+        self.foc_finished++;
7047+        self.foc_john = {john: john, created: created};
7048+      }
7049+    });
7050+  };
7051
7052+  this.test_waitForFindOrCreate = {
7053+    method: "waits.forJS",
7054+    params: {
7055+      js: function () { return self.foc_finished === 2 }
7056+    }
7057+  };
7058
7059+  this.test_verifyFindOrCreate = function () {
7060+    jum.assertEquals("jim", self.foc_jim.jim.id);
7061+    jum.assertTrue(self.foc_jim.created !== true);
7062+   
7063+    jum.assertEquals("john", self.foc_john.john.id);
7064+    jum.assertSameObjects(self.john_props, self.foc_john.john.doc);
7065+    jum.assertTrue(self.foc_john.created);
7066+  };
7067
7068+  this.test_destroy = function () {
7069+    self.success_on_destroy = self.failure_on_destroy = false;
7070+    self.jim.destroy(function () {
7071+      self.Person.findFirst({
7072+        properties: {id: "jim"},
7073+        onSuccess: function() {
7074+          self.success_on_destory = true;
7075+        },
7076+        onFailure: function () {
7077+          self.failure_on_destory = true;
7078+        }
7079+      });
7080+    });
7081+  };
7082
7083+  this.wait_forDestroy = {
7084+    method: "waits.forJS",
7085+    params: {
7086+      js: function () { return self.failure_on_destroy || self.success_on_destroy }
7087+    }
7088+  };
7089
7090+  this.test_verifyDestroy = function () {
7091+    jum.assertTrue(self.failure_on_destroy);
7092+    jum.assertFalse(self.success_on_destroy);
7093+  };
7094
7095+  this.teardown = function () {
7096+    self.db.wipe();
7097+  };
7098
7099+  return this;
7100+};
7101addfile ./contrib/musicplayer/tests/test_Goal.js
7102hunk ./contrib/musicplayer/tests/test_Goal.js 1
7103+var test_Goal = new function () {
7104+  var Goal = da.util.Goal,
7105+      self = this;
7106+  this.test_setup = function () {
7107+    this._timestamps = {};
7108+    this._calls = {a: 0, b: 0, c: 0, afterC: 0, success: 0, setup: 0};
7109+    this._calls.setup++;
7110+    this._goal = new Goal({
7111+      checkpoints: ["a", "b", "c"],
7112+     
7113+      onCheckpoint: function (name) {
7114+        self._timestamps[name] = new Date();
7115+        self._calls[name]++;
7116+      },
7117+     
7118+      onFinish: function (name) {
7119+        self._timestamps.success = new Date();
7120+        self._calls.success++;
7121+      },
7122+     
7123+      afterCheckpoint: {
7124+        c: function () {
7125+          self._timestamps.afterC = new Date();
7126+          self._calls.afterC++;
7127+        }
7128+      }
7129+    });
7130+   
7131+    this._goal.checkpoint("b");
7132+    this._goal.checkpoint("c");
7133+    this._goal.checkpoint("a");
7134
7135+    this._goal.checkpoint("c");
7136+    this._goal.checkpoint("b");
7137+  };
7138
7139+  this.test_allEventsCalledOnce = function () {
7140+    jum.assertTrue(this._calls.a        === 1);
7141+    jum.assertTrue(this._calls.b        === 1);
7142+    jum.assertTrue(this._calls.c        === 1);
7143+    jum.assertTrue(this._calls.afterC   === 1);
7144+    jum.assertTrue(this._calls.success  === 1);
7145+    jum.assertTrue(this._goal.finished);
7146+  };
7147
7148+  this.test_timestamps = function () {
7149+    jum.assertTrue(this._timestamps.b <= this._timestamps.c);
7150+    jum.assertTrue(this._timestamps.c <= this._timestamps.a);
7151+    jum.assertTrue(this._timestamps.c <= this._timestamps.afterC);
7152+  };
7153
7154+  this.test_successCalls = function () {
7155+    jum.assertTrue(this._timestamps.success >= this._timestamps.a);
7156+    jum.assertTrue(this._timestamps.success >= this._timestamps.b);
7157+    jum.assertTrue(this._timestamps.success >= this._timestamps.c);
7158+    jum.assertTrue(this._timestamps.success >= this._timestamps.afterC);
7159+  };
7160+};
7161addfile ./contrib/musicplayer/tests/test_ID3.js
7162hunk ./contrib/musicplayer/tests/test_ID3.js 1
7163+windmill.jsTest.require("shared.js");
7164+windmill.jsTest.require("data/songs.js");
7165+
7166+var test_ID3 = new function () {
7167+  var BinaryFile = da.util.BinaryFile,
7168+      ID3 = da.util.ID3;
7169
7170+  var ID3_patched = new Class({
7171+    Extends: ID3,
7172+   
7173+    _data: BinaryFile.fromEncodedString(SHARED.songs.image.data),
7174+    _getFile: function (parser) {
7175+      if(!parser)
7176+        this.options.onFailure();
7177+      else
7178+        this._onFileFetched(this._data);
7179+    }
7180+  });
7181+  ID3_patched.parsers = $A(ID3.parsers);
7182
7183+  var self = this;
7184+  this.setup = function () {
7185+    this.called_onSuccess = false;
7186+    this.called_onFailure = false;
7187+   
7188+    new ID3_patched({
7189+      url: "/fake/" + Math.uuid(),
7190+      onSuccess: function () {
7191+        self.called_onSuccess = true;
7192+      },
7193+      onFailure: function () {
7194+        self.called_onFailure = true;
7195+      }
7196+    });
7197+  };
7198
7199+  this.test_callbacks = function () {
7200+    jum.assertTrue(self.called_onFailure);
7201+    jum.assertFalse(self.called_onSuccess);
7202+  };
7203
7204+  this.teardown = function () {
7205+    delete self.called_onSuccess;
7206+    delete self.called_onFailure;
7207+  };
7208+};
7209addfile ./contrib/musicplayer/tests/test_ID3v1.js
7210hunk ./contrib/musicplayer/tests/test_ID3v1.js 1
7211+windmill.jsTest.require("shared.js");
7212+windmill.jsTest.require("data/songs.js");
7213+
7214+var test_ID3v1 = new function () {
7215+  var BinaryFile  = da.util.BinaryFile,
7216+      ID3v1Parser = da.util.ID3v1Parser,
7217+      self        = this;
7218
7219+  this.setup = function () {
7220+    this.tags = {};
7221+   
7222+    SHARED.parser = new ID3v1Parser(BinaryFile.fromEncodedString(SHARED.songs.v1.data), {
7223+      url: "/fake/" + Math.uuid(),
7224+      onSuccess: function (tags) {
7225+        self.tags = tags;
7226+      }
7227+    }, {});
7228+  };
7229
7230+  this.test_waitForData = {
7231+    method: 'waits.forJS',
7232+    params: {
7233+      js: function () { return !!self.tags; }
7234+    }
7235+  };
7236
7237+  this.test_verifyResult = function () {
7238+    jum.assertSameObjects(SHARED.songs.v1.simplified, self.tags);
7239+  };
7240
7241+  this.test_withID3v2 = function () {
7242+    jum.assertFalse("ID3v1 parser should not parse ID3v2 tags",
7243+      ID3v1Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v24.data))
7244+    );
7245+  };
7246
7247+  this.test_withPNGFile = function () {
7248+    jum.assertFalse("ID3v1 parser should not parse PNG file",
7249+      ID3v1Parser.test(BinaryFile.fromEncodedString(SHARED.songs.image.data))
7250+    );
7251+  };
7252+};
7253addfile ./contrib/musicplayer/tests/test_ID3v2.js
7254hunk ./contrib/musicplayer/tests/test_ID3v2.js 1
7255+windmill.jsTest.require("shared.js");
7256+windmill.jsTest.require("data/songs.js");
7257+
7258+var test_ID3v2 = new function () {
7259+  var BinaryFile = da.util.BinaryFile,
7260+      ID3v2Parser = da.util.ID3v2Parser;
7261
7262+  // Sometimes the code gets exectued before data/songs.js
7263+  this.test_waitForData = {
7264+    method: "waits.forJS",
7265+    params: {
7266+      js: function () { return !!SHARED && !!SHARED.songs }
7267+    }
7268+  };
7269
7270+  this.test_withPNGFile = function () {
7271+    jum.assertFalse("should not parse PNG file",
7272+      ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.image.data))
7273+    );
7274+  };
7275
7276+  this.test_withID3v1File = function () {
7277+    jum.assertFalse("should not parse ID3v1 file",
7278+      ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v1.data))
7279+    );
7280+  };
7281
7282+  this.test_withID3v2Files = function () {
7283+    jum.assertTrue("should detect v2.2",
7284+      ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v22.data))
7285+    );
7286+    jum.assertTrue("should detect v2.3",
7287+      ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v23.data))
7288+    );
7289+    jum.assertTrue(ID3v2Parser.test(BinaryFile.fromEncodedString(SHARED.songs.v24.data)));
7290+  };
7291
7292+  return this;
7293+};
7294+
7295+var test_ID3v22 = util.create_id3v2_test(2.2, 25792);
7296+var test_ID3v23 = util.create_id3v2_test(2.3, 10379);
7297+var test_ID3v24 = util.create_id3v2_test(2.4, 266);
7298addfile ./contrib/musicplayer/tests/test_Menu.js
7299hunk ./contrib/musicplayer/tests/test_Menu.js 1
7300-
7301+var test_Menu = new function () {
7302+  var Menu = da.ui.Menu,
7303+      self = this;
7304
7305+  this.setup = function () {
7306+    self.menu = new Menu({
7307+      items: {
7308+        a: {html: "a", id: "_test_first_menu_item"},
7309+        b: {html: "b", id: "_test_second_menu_item"},
7310+        _sep: Menu.separator,
7311+        c: {html: "c", id: "_test_third_menu_item"}
7312+      }
7313+    });
7314+  };
7315
7316+  this.test_domNode = function () {
7317+    var el = self.menu.toElement();
7318+   
7319+    jum.assertEquals("menu's element should be inserted into body of the page",
7320+      el.getParent(), document.body
7321+    );
7322+    //jum.assertEquals("should have four list items", )
7323+  };
7324
7325+  this.test_events = function () {
7326+    var el = self.menu.toElement();
7327+   
7328+    self.menu.addEvent("click", function (key, element) {
7329+      jum.assertEquals("clicked items' key should be 'b'", "b", key);
7330+    });
7331+    // events are synchronous
7332+    self.menu.click(null, el.getElement("li:nth-child(2)"));
7333+   
7334+    var showed = 0,
7335+        hidden = 0;
7336+    self.menu.addEvent("show", function () {
7337+      showed++;
7338+    });
7339+    self.menu.addEvent("hide", function () {
7340+      hidden++;
7341+    });
7342+   
7343+    self.menu.show();
7344+    jum.assertEquals("shown menu should be visible to the user",
7345+      "block", el.style.display
7346+    );
7347+   
7348+    self.menu.show();
7349+    jum.assertEquals("showing visible menu should not fire 'show' event ",
7350+      1, showed
7351+    );
7352+    jum.assertEquals("calling `show` on visible menu should hide it",
7353+      "none", el.style.display
7354+    );
7355+   
7356+    self.menu.hide();
7357+    jum.assertEquals("hiding hidden menu should not fire 'hide' event",
7358+      1, hidden
7359+    );
7360+  };
7361
7362+  this.teardown = function () {
7363+    self.menu.destroy();
7364+  };
7365
7366+  return this;
7367+};
7368addfile ./contrib/musicplayer/tests/test_NavigationController.js
7369hunk ./contrib/musicplayer/tests/test_NavigationController.js 1
7370+var test_NavigationController = new function () {
7371+  var Navigation = da.controller.Navigation,
7372+      self = this;
7373
7374+  // We can't use da.controller.CollectionScanner.isFinished()
7375+  // here because scanner worker has one minute timeout
7376+  this.test_waitForCollectionScanner = {
7377+    method: "waits.forJS",
7378+    params: {
7379+      js: function () {
7380+        return da.db.DEFAULT.views.Song.view.rows.length === 3
7381+      }
7382+    }
7383+  };
7384
7385+  // Generated by Windmill
7386+  // It clicks on a item in Artists column and than on a item in Albums column
7387+  this.test_navigationBehaviour = [
7388+    {"params": {"xpath": "//div[@id='Artists_column_container']/div/div[2]/a[2]/span"},
7389+      "method": "click"},
7390+    {"params": {"xpath": "//div[@id='Albums_column_container']/div/div[2]/a/span"},
7391+     "method": "click"},
7392+    {"params": {"xpath": "//div[@id='Albums_column_container']/a/span", "validator": "Albums"},
7393+     "method": "asserts.assertText"}
7394+  ];
7395
7396+  this.test_activeColumns = function () {
7397+    var ac = Navigation.activeColumns;
7398+    jum.assertEquals("first column should be Root",
7399+      "Root", ac[0].column_name
7400+    );
7401+    jum.assertEquals("second column should be Artists",
7402+      "Artists", ac[1].column_name
7403+    );
7404+    jum.assertEquals("third colum should be Albums",
7405+      "Albums", ac[2].column_name
7406+    );
7407+    jum.assertEquals("fourth column should be Songs",
7408+      "Songs", ac[3].column_name
7409+    );
7410+  };
7411
7412+  this.test_items = function () {
7413+    var ac      = Navigation.activeColumns,
7414+        artists = ac[1].column,
7415+        albums  = ac[2].column,
7416+        songs   = ac[3].column;
7417+   
7418+    jum.assertEquals("there should be two artists",
7419+      2, artists.options.totalCount
7420+    );
7421+    jum.assertEquals("first artist should be Keane",
7422+      "Keane", artists.getItem(0).value.title
7423+    );
7424+    jum.assertEquals("second artist should be Superhumanoids",
7425+      "Superhumanoids", artists.getItem(1).value.title
7426+    );
7427+   
7428+    jum.assertEquals("there should be only one album by Superhumanoids",
7429+      1, albums.options.totalCount
7430+    );
7431+    jum.assertEquals("first album should be Urgency",
7432+      "Urgency", albums.getItem(0).value.title
7433+    );
7434+   
7435+    jum.assertEquals("there should be two songs on Urgency album",
7436+      2, songs.options.totalCount
7437+    );
7438+    // indirectly tests sorting, since 'Hey Big Bang' is third track
7439+    // while 'Persona' is first on the album
7440+    jum.assertEquals("first song should be 'Persona'",
7441+      "Persona", songs.getItem(0).value.title
7442+    );
7443+    jum.assertEquals("second song should be 'Hey Big Bang'",
7444+      "Hey Big Bang", songs.getItem(1).value.title
7445+    );
7446+  };
7447
7448+  return this;
7449+};
7450addfile ./contrib/musicplayer/tests/test_utils.js
7451hunk ./contrib/musicplayer/tests/test_utils.js 1
7452+var test_StringStrip = function () {
7453+  jum.assertEquals("123ab", "123\0\0a\0\0b".strip());
7454+  jum.assertEquals("abc",   "\0\0\0ab\0c\0\0\0".strip());
7455+  jum.assertEquals("d",     "\0d".strip());
7456+  jum.assertEquals("e ",    "e\0 ".strip());
7457+};
7458+
7459+var test_StringInterpolate = new function () {
7460+  this.test_withNoArgs = function () {
7461+    jum.assertEquals("test", "test".interpolate());
7462+  };
7463
7464+  this.test_withArray = function () {
7465+    jum.assertEquals("10/100%",   "{0}/{1}%".interpolate([10, 100]));
7466+    jum.assertEquals("100/100%",  "{1}/{1}%".interpolate([10, 100]));
7467+    jum.assertEquals("001011",    "{0}{0}{1}{0}{1}{1}".interpolate([0, 1]));
7468+  };
7469
7470+  this.test_withObject = function () {
7471+    jum.assertEquals("Hi John! How are you?", "Hi {name}! How are {who}?".interpolate({
7472+      name: "John",
7473+      who: "you"
7474+    }));
7475+  };
7476
7477+  this.test_missingProperties = function () {
7478+    jum.assertEquals("Hi mum! {feeling} to see you!", "Hi {who}! {feeling} to see you!".interpolate({
7479+      who: "mum"
7480+    }));
7481+  };
7482+};
7483+
7484+var test_ArrayZip = new function () {
7485+  this.test_oneArg = function () {
7486+    jum.assertSameObjects([[1]], Array.zip([1]));
7487+  };
7488
7489+  this.test_twoArgs = function () {
7490+    jum.assertSameObjects([[1, 1], [2, 2], [3, 3]], Array.zip([1, 2, 3], [1, 2, 3]));
7491+  };
7492
7493+  this.test_moreSimpleArgs = function () {
7494+    jum.assertSameObjects([[1, 2, 3, 4, 5]], Array.zip([1], [2], [3], [4], [5]));
7495+  };
7496
7497+  this.test_notSameLength = function () {
7498+    jum.assertSameObjects([[1, 2, 4]], Array.zip([1], [2, 3], [4, 5, 6]));
7499+    jum.assertSameObjects([
7500+      [1, 4, 6],
7501+      [2, 5, undefined],
7502+      [3, undefined, undefined]
7503+    ], Array.zip([1, 2, 3], [4, 5], [6]));
7504+  };
7505+};
7506+
7507+var test_ArrayContainsAll = function () {
7508+  jum.assertTrue( []        .containsAll([]       ));
7509+  jum.assertTrue( [1, 2, 3] .containsAll([1, 2, 3]));
7510+  jum.assertTrue( [1, 2, 1] .containsAll([2, 1]   ));
7511+  jum.assertTrue( [1, 2]    .containsAll([1, 2, 1]));
7512+  jum.assertFalse([1, 2]    .containsAll([3, 1, 2]));
7513+};
7514+                                         
7515+var test_HashContainsAll = function () {
7516+  jum.assertTrue( $H({})                .containsAll({}          ));
7517+  jum.assertTrue( $H({a: 1, b: 2, c: 3}).containsAll({a: 1, b: 2}));
7518+  jum.assertFalse($H({a: 1})            .containsAll({a: 1, b: 2}));
7519+  jum.assertFalse($H({a: 1, b: 2})      .containsAll({a: 2, b: 3}));
7520+};
7521addfile ./src/allmydata/test/test_musicplayer.py
7522hunk ./src/allmydata/test/test_musicplayer.py 1
7523+import os, shutil
7524+from allmydata.test import tilting
7525+from allmydata.immutable import upload
7526+from base64 import b64decode
7527+
7528+timeout = 1200
7529+
7530+DATA = {}
7531+DATA['persona'] = """
7532+SUQzAwAAAAAGEFRJVDIAAAAJAAAAUGVyc29uYQBUUEUxAAAAEAAAAFN1cGVyaHVtYW5
7533+vaWRzAFRBTEIAAAAJAAAAVXJnZW5jeQBUUkNLAAAABQAAADEvNgBUWUVSAAAABgAAAD
7534+IwMTAAVENPTgAAAAYAAAAoMTMpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7535+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7536+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7537+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7538+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7539+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7540+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7541+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7542+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7543+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7544+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7545+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7546+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7547+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=""".replace('\n', '')
7548+
7549+DATA['bigbang'] = """
7550+SUQzAwAAAAACblRJVDIAAAAOAAAASGV5IEJpZyBCYW5nAFRQRTEAAAAQAAAAU3VwZXJo
7551+dW1hbm9pZHMAVEFMQgAAAAkAAABVcmdlbmN5AFRSQ0sAAAAFAAAAMy82AFRZRVIAAAAG
7552+AAAAMjAxMABUQ09OAAAABgAAACgxMykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7553+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7554+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7555+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7556+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7557+AAAAAAAAAAAAAAAAAAAAAAAAAA==""".replace('\n', '')
7558+
7559+DATA['maps'] = """
7560+SUQzBAAAAAAKClRJVDIAAAAFAAADTWFwc1RQRTEAAAAGAAADS2VhbmVURFJDAAAABQAA
7561+AzIwMTBUQUxCAAAAHwAAA1N1bnNoaW5lIFJldHJvc3BlY3RpdmUgQ29sbGVjdFRSQ0sA
7562+AAACAAADMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7563+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7564+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7565+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7566+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7567+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7568+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7569+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7570+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7571+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7572+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7573+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7574+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7575+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7576+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7577+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7578+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7579+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7580+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7581+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7582+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7583+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7584+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7585+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==""".replace('\n', '')
7586+
7587+class MusicPlayerJSTest:
7588+  def _set_up_tree(self):
7589+    self.settings['JAVASCRIPT_TEST_DIR'] = '../contrib/musicplayer/tests'
7590+    self.settings['SCRIPT_APPEND_ONLY'] = True
7591+   
7592+    self.test_url = 'static/musicplayer/index_devel.html'
7593+    shutil.copytree('../contrib/musicplayer/src', self.public_html_path + '/musicplayer')
7594+    #os.makedirs(self.public_html_path + '/musicplayer/js/workers')
7595+    shutil.copytree('../contrib/musicplayer/build/js/workers', self.public_html_path + '/musicplayer/js/workers')
7596+   
7597+    d = self.client.create_dirnode()
7598+    def _created_music_dirnode(node):
7599+      self.music_node = node
7600+      self.music_cap = node.get_uri()
7601+
7602+      return self.client.create_dirnode()
7603+    d.addCallback(_created_music_dirnode)
7604+   
7605+    def _created_settings_dirnode(node):
7606+      self.settings_cap = node.get_uri()
7607+    d.addCallback(_created_settings_dirnode)
7608+   
7609+    def _write_config_file(ign):
7610+      config = open(os.path.join(self.public_html_path, 'musicplayer', 'config.json'), 'w+')
7611+      config.write("""{
7612+        "music_cap":    "%s",
7613+        "settings_cap": "%s"
7614+      }\n""" % (self.music_cap, self.settings_cap))
7615+      config.close()
7616+    d.addCallback(_write_config_file)
7617+
7618+    persona = upload.Data(b64decode(DATA['persona']), None)
7619+    d.addCallback(lambda ign: self.music_node.add_file(u'persona', persona))
7620+
7621+    bigbang = upload.Data(b64decode(DATA['bigbang']), None)
7622+    d.addCallback(lambda ign: self.music_node.add_file(u'bigbang', bigbang))
7623+
7624+    maps = upload.Data(b64decode(DATA['maps']), None)
7625+    d.addCallback(lambda ign: self.music_node.add_file(u'maps',    maps))
7626+
7627+    return d
7628+
7629+class ChromeTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Chrome):
7630+  pass
7631+
7632+#  .
7633+#class FirefoxTest(MusicPlayerJSTest, tilting.JSTestsMixin, tilting.Firefox):
7634+  #pass
7635addfile ./src/allmydata/test/tilting.py
7636hunk ./src/allmydata/test/tilting.py 1
7637+# Note: may be Apache 2.0 license-contaminated.
7638+# (I haven't checked whether there is a significant license compatibility issue here.)
7639+
7640+import os, logging, tempfile, windmill
7641+from windmill.bin import admin_lib
7642+from twisted.internet import defer
7643+from twisted.trial import unittest
7644+from foolscap.api import eventually
7645+
7646+from allmydata.util import log, fileutil
7647+from allmydata.scripts.create_node import create_node, create_introducer
7648+from allmydata.scripts.startstop_node import do_start, do_stop
7649+from allmydata.immutable import upload
7650+from allmydata.test.no_network import GridTestMixin
7651+
7652+from time import sleep
7653+
7654+class TiltingMixin(GridTestMixin):
7655+  # adapted from
7656+  # http://github.com/windmill/windmill/blob/master/windmill/authoring/unit.py
7657+  # http://github.com/windmill/windmill/blob/master/windmill/bin/shell_objects.py
7658
7659+  def _set_up(self, basedir, num_clients=1, num_servers=10):
7660+    self.basedir = 'tilting/' + basedir
7661+    self.set_up_grid(num_clients=num_clients, num_servers=num_servers)
7662+    self.client = self.g.clients[0]
7663+
7664+    self._set_up_windmill()
7665+    d = defer.maybeDeferred(self._set_up_tree)
7666+    d.addCallback(lambda ign: self._start_windmill())
7667+    return d
7668
7669+  def _set_up_windmill(self):
7670+    self.browser_debugging = True
7671+    self.browser_name = 'firefox'
7672+    self.test_url = '/'
7673+    self._js_test_details = []
7674+    self.settings = {
7675+      'EXIT_ON_DONE': False,
7676+      'CONSOLE_LOG_LEVEL': logging.CRITICAL,
7677+      'controllers': []}
7678+   
7679+    self.public_html_path = "public_html"
7680+    fileutil.make_dirs(self.public_html_path)
7681+   
7682+    log.msg("setting up Windmill for browser '%s'" % (self.browser_name))
7683+    windmill.block_exit = True
7684+    # Windmill loves to output all sorts of stuff
7685+    windmill.stdout = tempfile.TemporaryFile()
7686+    admin_lib.configure_global_settings(logging_on=False)
7687+    self.configure()
7688
7689+  def _start_windmill(self):   
7690+    if self.browser_name == 'firefox':
7691+      self.settings['INSTALL_FIREBUG'] = True
7692+    for (setting, value) in self.settings.iteritems():
7693+      windmill.settings[setting] = value
7694+    windmill.settings['TEST_URL'] = self.client_baseurls[0] + self.test_url
7695+   
7696+    self.shell_objects = admin_lib.setup()
7697+    self.jsonrpc = self.shell_objects['httpd'].jsonrpc_methods_instance
7698+    self.jsonrpc_app = self.shell_objects['httpd'].namespaces['windmill-jsonrpc']
7699+   
7700+    d = defer.Deferred()
7701+    # Windmill prints success/failure statistics on its own
7702+    # and this unfortunately seems to be the only way to stop it from doing that.
7703+    # This is just a stripped down version of teardown method from windmill.server.convergence.JSONRPCMethods
7704+    def _windmill_teardown(**kwargs):
7705+      if windmill.settings['EXIT_ON_DONE']:
7706+        admin_lib.teardown(admin_lib.shell_objects_dict)
7707+        windmill.runserver_running = False
7708+        sleep(.25)
7709+     
7710+      eventually(d.callback, None)
7711+   
7712+    self.jsonrpc_app.__dict__[u'teardown'] = _windmill_teardown
7713+   
7714+    log.msg("starting browser")
7715+    self.shell_objects['start_' + self.browser_name]()
7716+   
7717+    if self.browser_debugging:
7718+      ready_d = defer.Deferred()
7719+      admin_lib.on_ide_awake.append(lambda: eventually(ready_d.callback, None))
7720
7721+      self.xmlrpc = windmill.tools.make_xmlrpc_client()
7722+      ready_d.addCallback(lambda ign:
7723+        self.xmlrpc.add_command({'method':'commands.setOptions',
7724+                                 'params':{'runTests':False, 'priority':'normal'}}))
7725+   
7726+    if self.settings['JAVASCRIPT_TEST_DIR']:
7727+      self._log_js_test_results()
7728+   
7729+    return d
7730
7731+  def tearDown(self):
7732+    if self.browser_debugging:
7733+      self.xmlrpc.add_command({'method':'commands.setOptions',
7734+                               'params':{'runTests':True, 'priority':'normal'}})
7735+    else:
7736+      log.msg("shutting down browser '%s'" % (self.browser_name))
7737+      admin_lib.teardown(self.shell_objects)
7738+      log.msg("browser shutdown done")
7739+   
7740+    return GridTestMixin.tearDown(self)
7741
7742+  def _log_js_test_results(self):
7743+    # When running JS tests in windmill, only a "X tests of Y failed" string is printed
7744+    # when all tests finish. This replaces Windmill's reporting method so that
7745+    # all test results (success/failure) are collected in self._js_test_details and later reported
7746+    # to Trial via self.failUnless(). This way Trial can easily pickup failing tests
7747+    # and display the error messages (if any).
7748+   
7749+    def _report_without_resolve(**kwargs):
7750+      self.jsonrpc._test_resolution_suite.report_without_resolve(*kwargs)     
7751+      self._js_test_details.append(kwargs)
7752+     
7753+      return 200
7754+   
7755+    del self.jsonrpc_app.__dict__[u'report_without_resolve']
7756+    self.jsonrpc_app.register_method(_report_without_resolve, u'report_without_resolve')
7757+
7758+class JSTestsMixin:
7759+  """
7760+  Mixin for running tests written in JavaScript.
7761+  Remember to set self.settings['JS_TESTS_DIR'] (path is relative to _trial_tmp) as well as self.test_url.
7762+  """
7763
7764+  def test_js(self):
7765+    d = self._set_up('test_js')
7766+    d.addCallback(lambda ign: self._report_results())
7767+    return d
7768
7769+  def _report_results(self):
7770+    for test in self._js_test_details:
7771+      self.failUnless(test['result'], test['debug'])
7772+
7773+class Chrome(TiltingMixin, unittest.TestCase):
7774+  """Starts tests in Chrome."""
7775+  def configure(self):
7776+    self.browser_name = "chrome"
7777+
7778+class Firefox(TiltingMixin, unittest.TestCase):
7779+  """Starts tests in Firefox."""
7780+  def configure(self):
7781+    self.browser_name = "firefox"
7782+
7783+class InternetExplorer(TiltingMixin, unittest.TestCase):
7784+  """Starts tests in Internet Explorer."""
7785+  def configure(self):
7786+    self.browser_name = "ie"
7787+
7788+class Safari(TiltingMixin, unittest.TestCase):
7789+  """Starts tests in Safari."""
7790+  def configure(self):
7791+    self.browser_name = "safari"
7792}
7793
7794Context:
7795
7796[quickstart.html: python 2.5 -> 2.6 as recommended version
7797david-sarah@jacaranda.org**20100705175858
7798 Ignore-this: bc3a14645ea1d5435002966ae903199f
7799] 
7800[SFTP: don't call .stopProducing on the producer registered with OverwriteableFileConsumer (which breaks with warner's new downloader).
7801david-sarah@jacaranda.org**20100628231926
7802 Ignore-this: 131b7a5787bc85a9a356b5740d9d996f
7803] 
7804[docs/how_to_make_a_tahoe-lafs_release.txt: trivial correction, install.html should now be quickstart.html.
7805david-sarah@jacaranda.org**20100625223929
7806 Ignore-this: 99a5459cac51bd867cc11ad06927ff30
7807] 
7808[setup: in the Makefile, refuse to upload tarballs unless someone has passed the environment variable "BB_BRANCH" with value "trunk"
7809zooko@zooko.com**20100619034928
7810 Ignore-this: 276ddf9b6ad7ec79e27474862e0f7d6
7811] 
7812[trivial: tiny update to in-line comment
7813zooko@zooko.com**20100614045715
7814 Ignore-this: 10851b0ed2abfed542c97749e5d280bc
7815 (I'm actually committing this patch as a test of the new eager-annotation-computation of trac-darcs.)
7816] 
7817[docs: about.html link to home page early on, and be decentralized storage instead of cloud storage this time around
7818zooko@zooko.com**20100619065318
7819 Ignore-this: dc6db03f696e5b6d2848699e754d8053
7820] 
7821[docs: update about.html, especially to have a non-broken link to quickstart.html, and also to comment out the broken links to "for Paranoids" and "for Corporates"
7822zooko@zooko.com**20100619065124
7823 Ignore-this: e292c7f51c337a84ebfeb366fbd24d6c
7824] 
7825[TAG allmydata-tahoe-1.7.0
7826zooko@zooko.com**20100619052631
7827 Ignore-this: d21e27afe6d85e2e3ba6a3292ba2be1
7828] 
7829Patch bundle hash:
7830bdbbdc1c68b20ad0e265eb9b0afd16e4ac7c678d