PageRenderTime 332ms CodeModel.GetById 51ms app.highlight 193ms RepoModel.GetById 58ms app.codeStats 1ms

/data/js/tomahawk.js

http://github.com/tomahawk-player/tomahawk
JavaScript | 1790 lines | 1524 code | 122 blank | 144 comment | 171 complexity | 8bab3f3e08bdb05bb5c93e82036a7076 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
   2 *
   3 *   Copyright 2011,      Dominik Schmidt <domme@tomahawk-player.org>
   4 *   Copyright 2011-2012, Leo Franchi <lfranchi@kde.org>
   5 *   Copyright 2011,      Thierry Goeckel
   6 *   Copyright 2013,      Teo Mrnjavac <teo@kde.org>
   7 *   Copyright 2013-2014, Uwe L. Korn <uwelk@xhochy.com>
   8 *   Copyright 2014,      Enno Gottschalk <mrmaffen@googlemail.com>
   9 *
  10 *   Permission is hereby granted, free of charge, to any person obtaining a copy
  11 *   of this software and associated documentation files (the "Software"), to deal
  12 *   in the Software without restriction, including without limitation the rights
  13 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14 *   copies of the Software, and to permit persons to whom the Software is
  15 *   furnished to do so, subject to the following conditions:
  16 *
  17 *   The above copyright notice and this permission notice shall be included in
  18 *   all copies or substantial portions of the Software.
  19 */
  20
  21// if run in phantomjs add fake Tomahawk environment
  22if ((typeof Tomahawk === "undefined") || (Tomahawk === null)) {
  23    var Tomahawk = {
  24        fakeEnv: function () {
  25            return true;
  26        },
  27        resolverData: function () {
  28            return {
  29                scriptPath: function () {
  30                    return "/home/tomahawk/resolver.js";
  31                }
  32            };
  33        },
  34        log: function () {
  35            console.log.apply(arguments);
  36        }
  37    };
  38}
  39
  40Tomahawk.apiVersion = "0.2.2";
  41
  42//Statuses considered a success for HTTP request
  43var httpSuccessStatuses = [200, 201];
  44
  45Tomahawk.error = console.error;
  46
  47// install RSVP error handler for uncaught(!) errors
  48RSVP.on('error', function (reason) {
  49    var resolverName = "";
  50    if (Tomahawk.resolver.instance) {
  51        resolverName = Tomahawk.resolver.instance.settings.name + " - ";
  52    }
  53    if (reason) {
  54        Tomahawk.error(resolverName + 'Uncaught error:', reason);
  55    } else {
  56        Tomahawk.error(resolverName + 'Uncaught error: error thrown from RSVP but it was empty');
  57    }
  58});
  59
  60/**
  61 * Compares versions strings
  62 * (version1 < version2) == -1
  63 * (version1 = version2) == 0
  64 * (version1 > version2) == 1
  65 */
  66Tomahawk.versionCompare = function (version1, version2) {
  67    var v1 = version1.split('.').map(function (item) {
  68        return parseInt(item);
  69    });
  70    var v2 = version2.split('.').map(function (item) {
  71        return parseInt(item);
  72    });
  73    var length = Math.max(v1.length, v2.length);
  74    var i = 0;
  75
  76    for (; i < length; i++) {
  77        if (typeof v1[i] == "undefined" || v1[i] === null) {
  78            if (typeof v2[i] == "undefined" || v2[i] === null) {
  79                // v1 == v2
  80                return 0;
  81            } else if (v2[i] === 0) {
  82                continue;
  83            } else {
  84                // v1 < v2
  85                return -1;
  86            }
  87        } else if (typeof v2[i] == "undefined" || v2[i] === null) {
  88            if (v1[i] === 0) {
  89                continue;
  90            } else {
  91                // v1 > v2
  92                return 1;
  93            }
  94        } else if (v2[i] > v1[i]) {
  95            // v1 < v2
  96            return -1;
  97        } else if (v2[i] < v1[i]) {
  98            // v1 > v2
  99            return 1;
 100        }
 101    }
 102    // v1 == v2
 103    return 0;
 104};
 105
 106/**
 107 * Check if this is at least specified tomahawk-api-version.
 108 */
 109Tomahawk.atLeastVersion = function (version) {
 110    return (Tomahawk.versionCompare(Tomahawk.apiVersion, version) >= 0);
 111};
 112
 113Tomahawk.resolver = {
 114    scriptPath: Tomahawk.resolverData().scriptPath
 115};
 116
 117Tomahawk.timestamp = function () {
 118    return Math.round(new Date() / 1000);
 119};
 120
 121Tomahawk.htmlDecode = (function () {
 122    // this prevents any overhead from creating the object each time
 123    var element = document.createElement('textarea');
 124
 125    function decodeHTMLEntities(str) {
 126        if (str && typeof str === 'string') {
 127            str = str.replace(/</g, "&lt;");
 128            str = str.replace(/>/g, "&gt;");
 129            element.innerHTML = str;
 130            str = element.textContent;
 131            element.textContent = '';
 132        }
 133
 134        return str;
 135    }
 136
 137    return decodeHTMLEntities;
 138})();
 139
 140Tomahawk.dumpResult = function (result) {
 141    var results = result.results;
 142    Tomahawk.log("Dumping " + results.length + " results for query " + result.qid + "...");
 143    for (var i = 0; i < results.length; i++) {
 144        Tomahawk.log(results[i].artist + " - " + results[i].track + " | " + results[i].url);
 145    }
 146
 147    Tomahawk.log("Done.");
 148};
 149
 150// javascript part of Tomahawk-Object API
 151Tomahawk.extend = function (object, members) {
 152    var F = function () {};
 153    F.prototype = object;
 154    var newObject = new F();
 155
 156    for (var key in members) {
 157        newObject[key] = members[key];
 158    }
 159
 160    return newObject;
 161};
 162
 163//Deprecated for 0.9 resolvers. Reporting resolver capabilities is no longer necessary.
 164var TomahawkResolverCapability = {
 165    NullCapability: 0,
 166    Browsable:      1,
 167    PlaylistSync:   2,
 168    AccountFactory: 4,
 169    UrlLookup:      8
 170};
 171
 172//Deprecated for 0.9 resolvers. Use Tomahawk.UrlType instead.
 173var TomahawkUrlType = {
 174    Any: 0,
 175    Playlist: 1,
 176    Track: 2,
 177    Album: 4,
 178    Artist: 8,
 179    Xspf: 16
 180};
 181
 182//Deprecated for 0.9 resolvers. Use Tomahawk.ConfigTestResultType instead.
 183var TomahawkConfigTestResultType = {
 184    Other: 0,
 185    Success: 1,
 186    Logout: 2,
 187    CommunicationError: 3,
 188    InvalidCredentials: 4,
 189    InvalidAccount: 5,
 190    PlayingElsewhere: 6,
 191    AccountExpired: 7
 192};
 193
 194/**
 195 * Resolver BaseObject, inherit it to implement your own resolver.
 196 */
 197var TomahawkResolver = {
 198    init: function () {
 199    },
 200    scriptPath: function () {
 201        return Tomahawk.resolverData().scriptPath;
 202    },
 203    getConfigUi: function () {
 204        return {};
 205    },
 206    getUserConfig: function () {
 207        return JSON.parse(window.localStorage[this.scriptPath()] || "{}");
 208    },
 209    saveUserConfig: function () {
 210        var configJson = JSON.stringify(Tomahawk.resolverData().config);
 211        window.localStorage[this.scriptPath()] = configJson;
 212        this.newConfigSaved();
 213    },
 214    newConfigSaved: function () {
 215    },
 216    resolve: function (qid, artist, album, title) {
 217        return {
 218            qid: qid
 219        };
 220    },
 221    search: function (qid, searchString) {
 222        return this.resolve(qid, "", "", searchString);
 223    },
 224    artists: function (qid) {
 225        return {
 226            qid: qid
 227        };
 228    },
 229    albums: function (qid, artist) {
 230        return {
 231            qid: qid
 232        };
 233    },
 234    tracks: function (qid, artist, album) {
 235        return {
 236            qid: qid
 237        };
 238    },
 239    collection: function () {
 240        return {};
 241    },
 242    _adapter_testConfig: function (config) {
 243        return RSVP.Promise.resolve(this.testConfig(config)).then(function () {
 244            return {result: Tomahawk.ConfigTestResultType.Success};
 245        });
 246    },
 247    testConfig: function () {
 248        this.configTest();
 249    },
 250    getStreamUrl: function (qid, url) {
 251        Tomahawk.reportStreamUrl(qid, url);
 252    }
 253};
 254
 255Tomahawk.Resolver = {
 256    init: function () {
 257    },
 258    scriptPath: function () {
 259        return Tomahawk.resolverData().scriptPath;
 260    },
 261    getConfigUi: function () {
 262        return {};
 263    },
 264    getUserConfig: function () {
 265        return JSON.parse(window.localStorage[this.scriptPath()] || "{}");
 266    },
 267    saveUserConfig: function () {
 268        window.localStorage[this.scriptPath()] = JSON.stringify(Tomahawk.resolverData().config);
 269        this.newConfigSaved(Tomahawk.resolverData().config);
 270    },
 271    newConfigSaved: function () {
 272    },
 273    testConfig: function () {
 274    },
 275    getStreamUrl: function (params) {
 276        return params;
 277    },
 278    getDownloadUrl: function (params) {
 279        return params;
 280    },
 281    resolve: function() {
 282    },
 283    _adapter_resolve: function (params) {
 284        return RSVP.Promise.resolve(this.resolve(params)).then(function (results) {
 285            if(Array.isArray(results)) {
 286                return {
 287                    'tracks': results
 288                };
 289            }
 290
 291            return results;
 292        });
 293    },
 294
 295    _adapter_search: function (params) {
 296        return RSVP.Promise.resolve(this.search(params)).then(function (results) {
 297            if(Array.isArray(results)) {
 298                return {
 299                    'tracks': results
 300                };
 301            }
 302
 303            return results;
 304        });
 305    },
 306
 307    _adapter_testConfig: function (config) {
 308        return RSVP.Promise.resolve(this.testConfig(config)).then(function (results) {
 309            results = results || Tomahawk.ConfigTestResultType.Success;
 310            return results;
 311        }, function (error) {
 312            return error;
 313        });
 314    }
 315};
 316
 317// help functions
 318
 319Tomahawk.valueForSubNode = function (node, tag) {
 320    if (node === undefined) {
 321        throw new Error("Tomahawk.valueForSubnode: node is undefined!");
 322    }
 323
 324    var element = node.getElementsByTagName(tag)[0];
 325    if (element === undefined) {
 326        return undefined;
 327    }
 328
 329    return element.textContent;
 330};
 331
 332/**
 333 * Internal counter used to identify retrievedMetadata call back from native
 334 * code.
 335 */
 336Tomahawk.retrieveMetadataIdCounter = 0;
 337/**
 338 * Internal map used to map metadataIds to the respective JavaScript callbacks.
 339 */
 340Tomahawk.retrieveMetadataCallbacks = {};
 341
 342/**
 343 * Retrieve metadata for a media stream.
 344 *
 345 * @param url String The URL which should be scanned for metadata.
 346 * @param mimetype String The mimetype of the stream, e.g. application/ogg
 347 * @param sizehint Size in bytes if not supplied possibly the whole file needs
 348 *          to be downloaded
 349 * @param options Object Map to specify various parameters related to the media
 350 *          URL. This includes:
 351 *          * headers: Object of HTTP(S) headers that should be set on doing the
 352 *                     request.
 353 *          * method: String HTTP verb to be used (default: GET)
 354 *          * username: Username when using authentication
 355 *          * password: Password when using authentication
 356 * @param callback Function(Object,String) This function is called on completeion.
 357 *          If an error occured, error is set to the corresponding message else
 358 *          null.
 359 */
 360Tomahawk.retrieveMetadata = function (url, mimetype, sizehint, options, callback) {
 361    var metadataId = Tomahawk.retrieveMetadataIdCounter;
 362    Tomahawk.retrieveMetadataIdCounter++;
 363    Tomahawk.retrieveMetadataCallbacks[metadataId] = callback;
 364    Tomahawk.nativeRetrieveMetadata(metadataId, url, mimetype, sizehint, options);
 365};
 366
 367/**
 368 * Pass the natively retrieved metadata back to the JavaScript callback.
 369 *
 370 * Internal use only!
 371 */
 372Tomahawk.retrievedMetadata = function (metadataId, metadata, error) {
 373    // Check that we have a matching callback stored.
 374    if (!Tomahawk.retrieveMetadataCallbacks.hasOwnProperty(metadataId)) {
 375        return;
 376    }
 377
 378    // Call the real callback
 379    if (Tomahawk.retrieveMetadataCallbacks.hasOwnProperty(metadataId)) {
 380        Tomahawk.retrieveMetadataCallbacks[metadataId](metadata, error);
 381    }
 382
 383    // Callback are only used once.
 384    delete Tomahawk.retrieveMetadataCallbacks[metadataId];
 385};
 386
 387/**
 388 * This method is externalized from Tomahawk.asyncRequest, so that other clients
 389 * (like tomahawk-android) can inject their own logic that determines whether or not to do a request
 390 * natively.
 391 *
 392 * @returns boolean indicating whether or not to do a request with the given parameters natively
 393 */
 394var shouldDoNativeRequest = function (options) {
 395    var extraHeaders = options.headers;
 396    return (extraHeaders && (extraHeaders.hasOwnProperty("Referer")
 397        || extraHeaders.hasOwnProperty("referer")
 398        || extraHeaders.hasOwnProperty("User-Agent")));
 399};
 400
 401/**
 402 * Possible options:
 403 *  - url: The URL to call
 404 *  - method: The HTTP request method (default: GET)
 405 *  - username: The username for HTTP Basic Auth
 406 *  - password: The password for HTTP Basic Auth
 407 *  - errorHandler: callback called if the request was not completed
 408 *  - data: body data included in POST requests
 409 *  - needCookieHeader: boolean indicating whether or not the request needs to be able to get the
 410 *                      "Set-Cookie" response header
 411 *  - headers: headers set on the request
 412 */
 413var doRequest = function(options) {
 414    if (shouldDoNativeRequest(options)) {
 415        return Tomahawk.NativeScriptJobManager.invoke('httpRequest', options).then(function(xhr) {
 416            xhr.responseHeaders = xhr.responseHeaders || {};
 417            xhr.getAllResponseHeaders = function() {
 418                return this.responseHeaders;
 419            };
 420            xhr.getResponseHeader = function (header) {
 421                for(key in xhr.responseHeaders) {
 422                    if(key.toLowerCase() === header.toLowerCase()) {
 423                        return xhr.responseHeaders[key];
 424                    }
 425                }
 426                return null;
 427            };
 428
 429            return xhr;
 430        });
 431    } else {
 432        return new RSVP.Promise(function(resolve, reject) {
 433            var xmlHttpRequest = new XMLHttpRequest();
 434            xmlHttpRequest.open(options.method, options.url, true, options.username, options.password);
 435            if (options.headers) {
 436                for (var headerName in options.headers) {
 437                    xmlHttpRequest.setRequestHeader(headerName, options.headers[headerName]);
 438                }
 439            }
 440            xmlHttpRequest.onreadystatechange = function () {
 441                if (xmlHttpRequest.readyState == 4
 442                    && httpSuccessStatuses.indexOf(xmlHttpRequest.status) != -1) {
 443                    resolve(xmlHttpRequest);
 444                } else if (xmlHttpRequest.readyState === 4) {
 445                    Tomahawk.log("Failed to do " + options.method + " request: to: " + options.url);
 446                    Tomahawk.log("Status Code was: " + xmlHttpRequest.status);
 447                    reject(xmlHttpRequest);
 448                }
 449            };
 450            xmlHttpRequest.send(options.data || null);
 451        });
 452    }
 453};
 454
 455Tomahawk.ajax = function (url, settings) {
 456    if (typeof url === "object") {
 457        settings = url;
 458    } else {
 459        settings = settings || {};
 460        settings.url = url;
 461    }
 462
 463    settings.type = settings.type || settings.method || 'get';
 464    settings.method = settings.type;
 465    settings.dataFormat = settings.dataFormat || 'form';
 466
 467    if (settings.data) {
 468        var formEncode = function (obj) {
 469            var str = [];
 470            for (var p in obj) {
 471                if (obj[p] !== undefined) {
 472                    if (Array.isArray(obj[p])) {
 473                        for (var i = 0; i < obj[p].length; i++) {
 474                            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p][i]));
 475                        }
 476                    } else {
 477                        str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
 478                    }
 479                }
 480            }
 481
 482            str.sort();
 483
 484            return str.join("&");
 485        };
 486        if (typeof settings.data === 'object') {
 487            if (settings.dataFormat == 'form') {
 488                settings.data = formEncode(settings.data);
 489                settings.contentType = settings.contentType || 'application/x-www-form-urlencoded';
 490            } else if (settings.dataFormat == 'json') {
 491                settings.data = JSON.stringify(settings.data);
 492                settings.contentType = settings.contentType || 'application/json';
 493            } else {
 494                throw new Error("Tomahawk.ajax: unknown dataFormat requested: "
 495                    + settings.dataFormat);
 496            }
 497        } else {
 498            throw new Error("Tomahawk.ajax: data should be either object or string");
 499        }
 500
 501        if (settings.type.toLowerCase() === 'get') {
 502            settings.url += '?' + settings.data;
 503            delete settings.data;
 504        } else {
 505            settings.headers = settings.headers || {};
 506            if (!settings.headers.hasOwnProperty('Content-Type')) {
 507                settings.headers['Content-Type'] = settings.contentType;
 508            }
 509        }
 510    }
 511
 512    return doRequest(settings).then(function (xhr) {
 513            if (settings.rawResponse) {
 514                return xhr;
 515            }
 516            var responseText = xhr.responseText;
 517            var contentType;
 518            if (settings.dataType === 'json') {
 519                contentType = 'application/json';
 520            } else if (settings.dataType === 'xml') {
 521                contentType = 'text/xml';
 522            } else if (typeof xhr.getResponseHeader !== 'undefined') {
 523                contentType = xhr.getResponseHeader('Content-Type');
 524            } else if (xhr.hasOwnProperty('contentType')) {
 525                contentType = xhr['contentType'];
 526            } else {
 527                contentType = 'text/html';
 528            }
 529
 530            if (~contentType.indexOf('application/json')) {
 531                return JSON.parse(responseText);
 532            }
 533
 534            if (~contentType.indexOf('text/xml')) {
 535                var domParser = new DOMParser();
 536                return domParser.parseFromString(responseText, "text/xml");
 537            }
 538
 539            return xhr.responseText;
 540        });
 541};
 542
 543Tomahawk.post = function (url, settings) {
 544    if (typeof url === "object") {
 545        settings = url;
 546    } else {
 547        settings = settings || {};
 548        settings.url = url;
 549    }
 550
 551    settings.method = 'POST';
 552
 553    return Tomahawk.ajax(settings);
 554};
 555
 556Tomahawk.get = function (url, settings) {
 557    return Tomahawk.ajax(url, settings);
 558};
 559
 560Tomahawk.assert = function (assertion, message) {
 561    Tomahawk.nativeAssert(assertion, message);
 562};
 563
 564Tomahawk.sha256 = Tomahawk.sha256 || function (message) {
 565        return CryptoJS.SHA256(message).toString(CryptoJS.enc.Hex);
 566    };
 567Tomahawk.md5 = Tomahawk.md5 || function (message) {
 568        return CryptoJS.MD5(message).toString(CryptoJS.enc.Hex);
 569    };
 570// Return a HMAC (md5) signature of the input text with the desired key
 571Tomahawk.hmac = function (key, message) {
 572    return CryptoJS.HmacMD5(message, key).toString(CryptoJS.enc.Hex);
 573};
 574
 575// Extracted from https://github.com/andrewrk/diacritics version 1.2.0
 576// Thanks to Andrew Kelley for this MIT-licensed diacritic removal code
 577// Initialisation / precomputation
 578(function() {
 579    var replacementList = [
 580        {base: ' ', chars: "\u00A0"},
 581        {base: '0', chars: "\u07C0"},
 582        {base: 'A', chars: "\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F"},
 583        {base: 'AA', chars: "\uA732"},
 584        {base: 'AE', chars: "\u00C6\u01FC\u01E2"},
 585        {base: 'AO', chars: "\uA734"},
 586        {base: 'AU', chars: "\uA736"},
 587        {base: 'AV', chars: "\uA738\uA73A"},
 588        {base: 'AY', chars: "\uA73C"},
 589        {base: 'B', chars: "\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0181"},
 590        {base: 'C', chars: "\uFF43\u24b8\uff23\uA73E\u1E08"},
 591        {base: 'D', chars: "\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018A\u0189\u1D05\uA779"},
 592        {base: 'Dh', chars: "\u00D0"},
 593        {base: 'DZ', chars: "\u01F1\u01C4"},
 594        {base: 'Dz', chars: "\u01F2\u01C5"},
 595        {base: 'E', chars: "\u025B\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E\u1D07"},
 596        {base: 'F', chars: "\uA77C\u24BB\uFF26\u1E1E\u0191\uA77B"},
 597        {base: 'G', chars: "\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E\u0262"},
 598        {base: 'H', chars: "\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D"},
 599        {base: 'I', chars: "\u24BE\uFF29\xCC\xCD\xCE\u0128\u012A\u012C\u0130\xCF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197"},
 600        {base: 'J', chars: "\u24BF\uFF2A\u0134\u0248\u0237"},
 601        {base: 'K', chars: "\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2"},
 602        {base: 'L', chars: "\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780"},
 603        {base: 'LJ', chars: "\u01C7"},
 604        {base: 'Lj', chars: "\u01C8"},
 605        {base: 'M', chars: "\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C\u03FB"},
 606        {base: 'N', chars: "\uA7A4\u0220\u24C3\uFF2E\u01F8\u0143\xD1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u019D\uA790\u1D0E"},
 607        {base: 'NJ', chars: "\u01CA"},
 608        {base: 'Nj', chars: "\u01CB"},
 609        {base: 'O', chars: "\u24C4\uFF2F\xD2\xD3\xD4\u1ED2\u1ED0\u1ED6\u1ED4\xD5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\xD6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\xD8\u01FE\u0186\u019F\uA74A\uA74C"},
 610        {base: 'OE', chars: "\u0152"},
 611        {base: 'OI', chars: "\u01A2"},
 612        {base: 'OO', chars: "\uA74E"},
 613        {base: 'OU', chars: "\u0222"},
 614        {base: 'P', chars: "\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754"},
 615        {base: 'Q', chars: "\u24C6\uFF31\uA756\uA758\u024A"},
 616        {base: 'R', chars: "\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782"},
 617        {base: 'S', chars: "\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784"},
 618        {base: 'T', chars: "\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786"},
 619        {base: 'Th', chars: "\u00DE"},
 620        {base: 'TZ', chars: "\uA728"},
 621        {base: 'U', chars: "\u24CA\uFF35\xD9\xDA\xDB\u0168\u1E78\u016A\u1E7A\u016C\xDC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244"},
 622        {base: 'V', chars: "\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245"},
 623        {base: 'VY', chars: "\uA760"},
 624        {base: 'W', chars: "\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72"},
 625        {base: 'X', chars: "\u24CD\uFF38\u1E8A\u1E8C"},
 626        {base: 'Y', chars: "\u24CE\uFF39\u1EF2\xDD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE"},
 627        {base: 'Z', chars: "\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762"},
 628        {base: 'a', chars: "\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250\u0251"},
 629        {base: 'aa', chars: "\uA733"},
 630        {base: 'ae', chars: "\u00E6\u01FD\u01E3"},
 631        {base: 'ao', chars: "\uA735"},
 632        {base: 'au', chars: "\uA737"},
 633        {base: 'av', chars: "\uA739\uA73B"},
 634        {base: 'ay', chars: "\uA73D"},
 635        {base: 'b', chars: "\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253\u0182"},
 636        {base: 'c', chars: "\u24D2\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184\u0043\u0106\u0108\u010A\u010C\u00C7\u0187\u023B"},
 637        {base: 'd', chars: "\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\u018B\u13E7\u0501\uA7AA"},
 638        {base: 'dh', chars: "\u00F0"},
 639        {base: 'dz', chars: "\u01F3\u01C6"},
 640        {base: 'e', chars: "\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u01DD"},
 641        {base: 'f', chars: "\u24D5\uFF46\u1E1F\u0192"},
 642        {base: 'ff', chars: "\uFB00"},
 643        {base: 'fi', chars: "\uFB01"},
 644        {base: 'fl', chars: "\uFB02"},
 645        {base: 'ffi', chars: "\uFB03"},
 646        {base: 'ffl', chars: "\uFB04"},
 647        {base: 'g', chars: "\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\uA77F\u1D79"},
 648        {base: 'h', chars: "\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265"},
 649        {base: 'hv', chars: "\u0195"},
 650        {base: 'i', chars: "\u24D8\uFF49\xEC\xED\xEE\u0129\u012B\u012D\xEF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131"},
 651        {base: 'j', chars: "\u24D9\uFF4A\u0135\u01F0\u0249"},
 652        {base: 'k', chars: "\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3"},
 653        {base: 'l', chars: "\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747\u026D"},
 654        {base: 'lj', chars: "\u01C9"},
 655        {base: 'm', chars: "\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F"},
 656        {base: 'n', chars: "\u24DD\uFF4E\u01F9\u0144\xF1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5\u043B\u0509"},
 657        {base: 'nj', chars: "\u01CC"},
 658        {base: 'o', chars: "\u24DE\uFF4F\xF2\xF3\xF4\u1ED3\u1ED1\u1ED7\u1ED5\xF5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\xF6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\xF8\u01FF\uA74B\uA74D\u0275\u0254\u1D11"},
 659        {base: 'oe', chars: "\u0153"},
 660        {base: 'oi', chars: "\u01A3"},
 661        {base: 'oo', chars: "\uA74F"},
 662        {base: 'ou', chars: "\u0223"},
 663        {base: 'p', chars: "\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755\u03C1"},
 664        {base: 'q', chars: "\u24E0\uFF51\u024B\uA757\uA759"},
 665        {base: 'r', chars: "\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783"},
 666        {base: 's', chars: "\u24E2\uFF53\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B\u0282"},
 667        {base: 'ss', chars: "\xDF"},
 668        {base: 't', chars: "\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787"},
 669        {base: 'th', chars: "\u00FE"},
 670        {base: 'tz', chars: "\uA729"},
 671        {base: 'u', chars: "\u24E4\uFF55\xF9\xFA\xFB\u0169\u1E79\u016B\u1E7B\u016D\xFC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289"},
 672        {base: 'v', chars: "\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C"},
 673        {base: 'vy', chars: "\uA761"},
 674        {base: 'w', chars: "\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73"},
 675        {base: 'x', chars: "\u24E7\uFF58\u1E8B\u1E8D"},
 676        {base: 'y', chars: "\u24E8\uFF59\u1EF3\xFD\u0177\u1EF9\u0233\u1E8F\xFF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF"},
 677        {base: 'z', chars: "\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763"}
 678    ];
 679
 680    Tomahawk.diacriticsMap = {};
 681    var i, j, chars;
 682    for (i = 0; i < replacementList.length; i += 1) {
 683        chars = replacementList[i].chars;
 684        for (j = 0; j < chars.length; j += 1) {
 685            Tomahawk.diacriticsMap[chars[j]] = replacementList[i].base;
 686        }
 687    }
 688})();
 689
 690Tomahawk.removeDiacritics = function (str) {
 691    return str.replace(/[^\u0000-\u007E]/g, function (c) {
 692        return Tomahawk.diacriticsMap[c] || c;
 693    });
 694};
 695
 696Tomahawk.localStorage = Tomahawk.localStorage || {
 697        setItem: function (key, value) {
 698            window.localStorage[key] = value;
 699        },
 700        getItem: function (key) {
 701            return window.localStorage[key];
 702        },
 703        removeItem: function (key) {
 704            delete window.localStorage[key];
 705        }
 706    };
 707
 708// some aliases
 709Tomahawk.setTimeout = Tomahawk.setTimeout || window.setTimeout;
 710Tomahawk.setInterval = Tomahawk.setInterval || window.setInterval;
 711Tomahawk.base64Decode = function (a) {
 712    return window.atob(a);
 713};
 714Tomahawk.base64Encode = function (b) {
 715    return window.btoa(b);
 716};
 717
 718Tomahawk.PluginManager = {
 719    wrapperPrefix: '_adapter_',
 720    objects: {},
 721    objectCounter: 0,
 722    identifyObject: function (object) {
 723        if (!object.hasOwnProperty('id')) {
 724            object.id = this.objectCounter++;
 725        }
 726
 727        return object.id;
 728    },
 729    registerPlugin: function (type, object) {
 730        this.objects[this.identifyObject(object)] = object;
 731        Tomahawk.log("registerPlugin: " + type + " id: " + object.id);
 732        Tomahawk.registerScriptPlugin(type, object.id);
 733    },
 734
 735    unregisterPlugin: function (type, object) {
 736        this.objects[this.identifyObject(object)] = object;
 737
 738        Tomahawk.log("unregisterPlugin: " + type + " id: " + object.id);
 739        Tomahawk.unregisterScriptPlugin(type, object.id);
 740    },
 741
 742    resolve: [],
 743    invokeSync: function (requestId, objectId, methodName, params) {
 744        if (this.objects[objectId][this.wrapperPrefix + methodName]) {
 745            methodName = this.wrapperPrefix + methodName;
 746        }
 747
 748        if (!this.objects[objectId]) {
 749            Tomahawk.log("Object not found! objectId: " + objectId + " methodName: " + methodName);
 750        } else {
 751            if (!this.objects[objectId][methodName]) {
 752                Tomahawk.log("Function not found: " + methodName);
 753            }
 754        }
 755
 756        if (typeof this.objects[objectId][methodName] !== 'function' && this.objects[objectId][methodName]) {
 757            return this.objects[objectId][methodName];
 758        } else if (typeof this.objects[objectId][methodName] !== 'function') {
 759            throw new Error('\'' + methodName + '\' on ScriptObject ' + objectId + ' is not a function', typeof this.objects[objectId][methodName]);
 760        }
 761
 762        return this.objects[objectId][methodName](params);
 763    },
 764
 765    invoke: function (requestId, objectId, methodName, params) {
 766        RSVP.Promise.resolve(this.invokeSync(requestId, objectId, methodName, params))
 767            .then(function (result) {
 768                var params = {
 769                    requestId: requestId,
 770                    data: result
 771                };
 772                Tomahawk.reportScriptJobResults(encodeParamsToNativeFunctions(params));
 773            }, function (error) {
 774                var params = {
 775                    requestId: requestId,
 776                    error: error
 777                };
 778                Tomahawk.reportScriptJobResults(encodeParamsToNativeFunctions(params));
 779            });
 780    }
 781};
 782
 783var encodeParamsToNativeFunctions = function(param) {
 784  return param;
 785};
 786
 787Tomahawk.NativeScriptJobManager = {
 788    idCounter: 0,
 789    deferreds: {},
 790    invoke: function (methodName, params) {
 791        params = params || {};
 792
 793        var requestId = this.idCounter++;
 794        var deferred = RSVP.defer();
 795        this.deferreds[requestId] = deferred;
 796        Tomahawk.invokeNativeScriptJob(requestId, methodName, encodeParamsToNativeFunctions(params));
 797        return deferred.promise;
 798    },
 799    reportNativeScriptJobResult: function(requestId, result) {
 800        var deferred = this.deferreds[requestId];
 801        if (!deferred) {
 802            Tomahawk.log("Deferred object with the given requestId is not present!");
 803        }
 804        deferred.resolve(result);
 805        delete this.deferreds[requestId];
 806    },
 807    reportNativeScriptJobError: function(requestId, error) {
 808        var deferred = this.deferreds[requestId];
 809        if (!deferred) {
 810            console.log("Deferred object with the given requestId is not present!");
 811        }
 812        deferred.reject(error);
 813        delete this.deferreds[requestId];
 814    }
 815};
 816
 817Tomahawk.UrlType = {
 818    Any: 0,
 819    Playlist: 1,
 820    Track: 2,
 821    Album: 3,
 822    Artist: 4,
 823    XspfPlaylist: 5
 824};
 825
 826Tomahawk.ConfigTestResultType = {
 827    Other: 0,
 828    Success: 1,
 829    Logout: 2,
 830    CommunicationError: 3,
 831    InvalidCredentials: 4,
 832    InvalidAccount: 5,
 833    PlayingElsewhere: 6,
 834    AccountExpired: 7
 835};
 836
 837Tomahawk.Country = {
 838    AnyCountry: 0,
 839    Afghanistan: 1,
 840    Albania: 2,
 841    Algeria: 3,
 842    AmericanSamoa: 4,
 843    Andorra: 5,
 844    Angola: 6,
 845    Anguilla: 7,
 846    Antarctica: 8,
 847    AntiguaAndBarbuda: 9,
 848    Argentina: 10,
 849    Armenia: 11,
 850    Aruba: 12,
 851    Australia: 13,
 852    Austria: 14,
 853    Azerbaijan: 15,
 854    Bahamas: 16,
 855    Bahrain: 17,
 856    Bangladesh: 18,
 857    Barbados: 19,
 858    Belarus: 20,
 859    Belgium: 21,
 860    Belize: 22,
 861    Benin: 23,
 862    Bermuda: 24,
 863    Bhutan: 25,
 864    Bolivia: 26,
 865    BosniaAndHerzegowina: 27,
 866    Botswana: 28,
 867    BouvetIsland: 29,
 868    Brazil: 30,
 869    BritishIndianOceanTerritory: 31,
 870    BruneiDarussalam: 32,
 871    Bulgaria: 33,
 872    BurkinaFaso: 34,
 873    Burundi: 35,
 874    Cambodia: 36,
 875    Cameroon: 37,
 876    Canada: 38,
 877    CapeVerde: 39,
 878    CaymanIslands: 40,
 879    CentralAfricanRepublic: 41,
 880    Chad: 42,
 881    Chile: 43,
 882    China: 44,
 883    ChristmasIsland: 45,
 884    CocosIslands: 46,
 885    Colombia: 47,
 886    Comoros: 48,
 887    DemocraticRepublicOfCongo: 49,
 888    PeoplesRepublicOfCongo: 50,
 889    CookIslands: 51,
 890    CostaRica: 52,
 891    IvoryCoast: 53,
 892    Croatia: 54,
 893    Cuba: 55,
 894    Cyprus: 56,
 895    CzechRepublic: 57,
 896    Denmark: 58,
 897    Djibouti: 59,
 898    Dominica: 60,
 899    DominicanRepublic: 61,
 900    EastTimor: 62,
 901    Ecuador: 63,
 902    Egypt: 64,
 903    ElSalvador: 65,
 904    EquatorialGuinea: 66,
 905    Eritrea: 67,
 906    Estonia: 68,
 907    Ethiopia: 69,
 908    FalklandIslands: 70,
 909    FaroeIslands: 71,
 910    FijiCountry: 72,
 911    Finland: 73,
 912    France: 74,
 913    MetropolitanFrance: 75,
 914    FrenchGuiana: 76,
 915    FrenchPolynesia: 77,
 916    FrenchSouthernTerritories: 78,
 917    Gabon: 79,
 918    Gambia: 80,
 919    Georgia: 81,
 920    Germany: 82,
 921    Ghana: 83,
 922    Gibraltar: 84,
 923    Greece: 85,
 924    Greenland: 86,
 925    Grenada: 87,
 926    Guadeloupe: 88,
 927    Guam: 89,
 928    Guatemala: 90,
 929    Guinea: 91,
 930    GuineaBissau: 92,
 931    Guyana: 93,
 932    Haiti: 94,
 933    HeardAndMcDonaldIslands: 95,
 934    Honduras: 96,
 935    HongKong: 97,
 936    Hungary: 98,
 937    Iceland: 99,
 938    India: 100,
 939    Indonesia: 101,
 940    Iran: 102,
 941    Iraq: 103,
 942    Ireland: 104,
 943    Israel: 105,
 944    Italy: 106,
 945    Jamaica: 107,
 946    Japan: 108,
 947    Jordan: 109,
 948    Kazakhstan: 110,
 949    Kenya: 111,
 950    Kiribati: 112,
 951    DemocraticRepublicOfKorea: 113,
 952    RepublicOfKorea: 114,
 953    Kuwait: 115,
 954    Kyrgyzstan: 116,
 955    Lao: 117,
 956    Latvia: 118,
 957    Lebanon: 119,
 958    Lesotho: 120,
 959    Liberia: 121,
 960    LibyanArabJamahiriya: 122,
 961    Liechtenstein: 123,
 962    Lithuania: 124,
 963    Luxembourg: 125,
 964    Macau: 126,
 965    Macedonia: 127,
 966    Madagascar: 128,
 967    Malawi: 129,
 968    Malaysia: 130,
 969    Maldives: 131,
 970    Mali: 132,
 971    Malta: 133,
 972    MarshallIslands: 134,
 973    Martinique: 135,
 974    Mauritania: 136,
 975    Mauritius: 137,
 976    Mayotte: 138,
 977    Mexico: 139,
 978    Micronesia: 140,
 979    Moldova: 141,
 980    Monaco: 142,
 981    Mongolia: 143,
 982    Montserrat: 144,
 983    Morocco: 145,
 984    Mozambique: 146,
 985    Myanmar: 147,
 986    Namibia: 148,
 987    NauruCountry: 149,
 988    Nepal: 150,
 989    Netherlands: 151,
 990    NetherlandsAntilles: 152,
 991    NewCaledonia: 153,
 992    NewZealand: 154,
 993    Nicaragua: 155,
 994    Niger: 156,
 995    Nigeria: 157,
 996    Niue: 158,
 997    NorfolkIsland: 159,
 998    NorthernMarianaIslands: 160,
 999    Norway: 161,
1000    Oman: 162,
1001    Pakistan: 163,
1002    Palau: 164,
1003    PalestinianTerritory: 165,
1004    Panama: 166,
1005    PapuaNewGuinea: 167,
1006    Paraguay: 168,
1007    Peru: 169,
1008    Philippines: 170,
1009    Pitcairn: 171,
1010    Poland: 172,
1011    Portugal: 173,
1012    PuertoRico: 174,
1013    Qatar: 175,
1014    Reunion: 176,
1015    Romania: 177,
1016    RussianFederation: 178,
1017    Rwanda: 179,
1018    SaintKittsAndNevis: 180,
1019    StLucia: 181,
1020    StVincentAndTheGrenadines: 182,
1021    Samoa: 183,
1022    SanMarino: 184,
1023    SaoTomeAndPrincipe: 185,
1024    SaudiArabia: 186,
1025    Senegal: 187,
1026    SerbiaAndMontenegro: 241,
1027    Seychelles: 188,
1028    SierraLeone: 189,
1029    Singapore: 190,
1030    Slovakia: 191,
1031    Slovenia: 192,
1032    SolomonIslands: 193,
1033    Somalia: 194,
1034    SouthAfrica: 195,
1035    SouthGeorgiaAndTheSouthSandwichIslands: 196,
1036    Spain: 197,
1037    SriLanka: 198,
1038    StHelena: 199,
1039    StPierreAndMiquelon: 200,
1040    Sudan: 201,
1041    Suriname: 202,
1042    SvalbardAndJanMayenIslands: 203,
1043    Swaziland: 204,
1044    Sweden: 205,
1045    Switzerland: 206,
1046    SyrianArabRepublic: 207,
1047    Taiwan: 208,
1048    Tajikistan: 209,
1049    Tanzania: 210,
1050    Thailand: 211,
1051    Togo: 212,
1052    Tokelau: 213,
1053    TongaCountry: 214,
1054    TrinidadAndTobago: 215,
1055    Tunisia: 216,
1056    Turkey: 217,
1057    Turkmenistan: 218,
1058    TurksAndCaicosIslands: 219,
1059    Tuvalu: 220,
1060    Uganda: 221,
1061    Ukraine: 222,
1062    UnitedArabEmirates: 223,
1063    UnitedKingdom: 224,
1064    UnitedStates: 225,
1065    UnitedStatesMinorOutlyingIslands: 226,
1066    Uruguay: 227,
1067    Uzbekistan: 228,
1068    Vanuatu: 229,
1069    VaticanCityState: 230,
1070    Venezuela: 231,
1071    VietNam: 232,
1072    BritishVirginIslands: 233,
1073    USVirginIslands: 234,
1074    WallisAndFutunaIslands: 235,
1075    WesternSahara: 236,
1076    Yemen: 237,
1077    Yugoslavia: 238,
1078    Zambia: 239,
1079    Zimbabwe: 240,
1080    Montenegro: 242,
1081    Serbia: 243,
1082    SaintBarthelemy: 244,
1083    SaintMartin: 245,
1084    LatinAmericaAndTheCaribbean: 246
1085};
1086
1087Tomahawk.Collection = {
1088    BrowseCapability: {
1089        Artists: 1,
1090        Albums: 2,
1091        Tracks: 4
1092    },
1093
1094    cachedDbs: {},
1095
1096    Transaction: function (collection, id) {
1097
1098        this.ensureDb = function () {
1099            return new RSVP.Promise(function (resolve, reject) {
1100                if (!collection.cachedDbs.hasOwnProperty(id)) {
1101                    Tomahawk.log("Opening database");
1102                    var estimatedSize = 5 * 1024 * 1024; // 5MB
1103                    collection.cachedDbs[id] =
1104                        openDatabase(id + "_collection", "", "Collection", estimatedSize);
1105
1106                    collection.cachedDbs[id].transaction(function (tx) {
1107                        Tomahawk.log("Creating initial db tables");
1108                        tx.executeSql("CREATE TABLE IF NOT EXISTS artists(" +
1109                            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1110                            "artist TEXT ," +
1111                            "artistDisambiguation TEXT," +
1112                            "UNIQUE (artist, artistDisambiguation) ON CONFLICT IGNORE)", []);
1113                        tx.executeSql("CREATE TABLE IF NOT EXISTS albumArtists(" +
1114                            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1115                            "albumArtist TEXT ," +
1116                            "albumArtistDisambiguation TEXT," +
1117                            "UNIQUE (albumArtist, albumArtistDisambiguation) ON CONFLICT IGNORE)",
1118                            []);
1119                        tx.executeSql("CREATE TABLE IF NOT EXISTS albums(" +
1120                            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1121                            "album TEXT," +
1122                            "albumArtistId INTEGER," +
1123                            "UNIQUE (album, albumArtistId) ON CONFLICT IGNORE," +
1124                            "FOREIGN KEY(albumArtistId) REFERENCES albumArtists(_id))", []);
1125                        tx.executeSql("CREATE TABLE IF NOT EXISTS artistAlbums(" +
1126                            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1127                            "albumId INTEGER," +
1128                            "artistId INTEGER," +
1129                            "UNIQUE (albumId, artistId) ON CONFLICT IGNORE," +
1130                            "FOREIGN KEY(albumId) REFERENCES albums(_id)," +
1131                            "FOREIGN KEY(artistId) REFERENCES artists(_id))", []);
1132                        tx.executeSql("CREATE TABLE IF NOT EXISTS tracks(" +
1133                            "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1134                            "track TEXT," +
1135                            "artistId INTEGER," +
1136                            "albumId INTEGER," +
1137                            "url TEXT," +
1138                            "duration INTEGER," +
1139                            "albumPos INTEGER," +
1140                            "linkUrl TEXT," +
1141                            'releaseyear INTEGER,' +
1142                            'bitrate INTEGER,' +
1143                            "UNIQUE (track, artistId, albumId) ON CONFLICT IGNORE," +
1144                            "FOREIGN KEY(artistId) REFERENCES artists(_id)," +
1145                            "FOREIGN KEY(albumId) REFERENCES albums(_id))", []);
1146                    });
1147                }
1148                resolve(collection.cachedDbs[id]);
1149            });
1150        };
1151
1152        this.beginTransaction = function () {
1153            var that = this;
1154            return this.ensureDb().then(function (db) {
1155                return new RSVP.Promise(function (resolve, reject) {
1156                    that.db = db;
1157                    that.statements = [];
1158                    resolve();
1159                })
1160            });
1161        };
1162
1163        this.execDeferredStatements = function (resolve, reject) {
1164            var that = this;
1165            that.stmtsToResolve = that.statements.length;
1166            that.results = that.statements.slice();
1167            Tomahawk.log('Executing ' + that.stmtsToResolve
1168                + ' deferred SQL statements in transaction');
1169            return new RSVP.Promise(function (resolve, reject) {
1170                if (that.statements.length == 0) {
1171                    resolve([]);
1172                } else {
1173                    that.db.transaction(function (tx) {
1174                        for (var i = 0; i < that.statements.length; ++i) {
1175                            var stmt = that.statements[i];
1176                            tx.executeSql(stmt.statement, stmt.args,
1177                                (function () {
1178                                    //A function returning a function to
1179                                    //capture value of i
1180                                    var originalI = i;
1181                                    return function (tx, results) {
1182                                        if (typeof that.statements[originalI].map !== 'undefined') {
1183                                            var map = that.statements[originalI].map;
1184                                            that.results[originalI] = [];
1185                                            for (var ii = 0; ii < results.rows.length; ii++) {
1186                                                that.results[originalI].push(map(
1187                                                    results.rows.item(ii)
1188                                                ));
1189                                            }
1190                                        }
1191                                        else {
1192                                            that.results[originalI] = results;
1193                                        }
1194                                        that.stmtsToResolve--;
1195                                        if (that.stmtsToResolve == 0) {
1196                                            that.statements = [];
1197                                            resolve(that.results);
1198                                        }
1199                                    };
1200                                })(), function (tx, error) {
1201                                    Tomahawk.log("Error in tx.executeSql: " + error.code + " - "
1202                                        + error.message);
1203                                    that.statements = [];
1204                                    reject(error);
1205                                }
1206                            );
1207                        }
1208                    });
1209                }
1210            });
1211        };
1212
1213        this.sql = function (sqlStatement, sqlArgs, mapFunction) {
1214            this.statements.push({statement: sqlStatement, args: sqlArgs, map: mapFunction});
1215        };
1216
1217        this.sqlSelect = function (table, mapResults, fields, where, join) {
1218            var whereKeys = [];
1219            var whereValues = [];
1220            for (var whereKey in where) {
1221                if (where.hasOwnProperty(whereKey)) {
1222                    whereKeys.push(table + "." + whereKey + " = ?");
1223                    whereValues.push(where[whereKey]);
1224                }
1225            }
1226            var whereString = whereKeys.length > 0 ? " WHERE " + whereKeys.join(" AND ") : "";
1227
1228            var joinString = "";
1229            for (var i = 0; join && i < join.length; i++) {
1230                var joinConditions = [];
1231                for (var joinKey in join[i].conditions) {
1232                    if (join[i].conditions.hasOwnProperty(joinKey)) {
1233                        joinConditions.push(table + "." + joinKey + " = " + join[i].table + "."
1234                            + join[i].conditions[joinKey]);
1235                    }
1236                }
1237                joinString += " INNER JOIN " + join[i].table + " ON "
1238                    + joinConditions.join(" AND ");
1239            }
1240
1241            var fieldsString = fields && fields.length > 0 ? fields.join(", ") : "*";
1242            var statement = "SELECT " + fieldsString + " FROM " + table + joinString + whereString;
1243            return this.sql(statement, whereValues, mapResults);
1244        };
1245
1246        this.sqlInsert = function (table, fields) {
1247            var fieldsKeys = [];
1248            var fieldsValues = [];
1249            var valuesString = "";
1250            for (var key in fields) {
1251                fieldsKeys.push(key);
1252                fieldsValues.push(fields[key]);
1253                if (valuesString.length > 0) {
1254                    valuesString += ", ";
1255                }
1256                valuesString += "?";
1257            }
1258            var statement = "INSERT INTO " + table + " (" + fieldsKeys.join(", ") + ") VALUES ("
1259                + valuesString + ")";
1260            return this.sql(statement, fieldsValues);
1261        };
1262
1263        this.sqlDrop = function (table) {
1264            var statement = "DROP TABLE IF EXISTS " + table;
1265            return this.sql(statement, []);
1266        };
1267
1268    },
1269
1270    addTracks: function (params) {
1271        var that = this;
1272        var id = params.id;
1273        var tracks = params.tracks;
1274
1275        var cachedAlbumArtists = {},
1276            cachedArtists = {},
1277            cachedAlbums = {},
1278            cachedArtistIds = {},
1279            cachedAlbumIds = {};
1280
1281        var t = new Tomahawk.Collection.Transaction(this, id);
1282        return t.beginTransaction().then(function () {
1283            // First we insert all artists and albumArtists
1284            t.sqlInsert("artists", {
1285                artist: "Various Artists",
1286                artistDisambiguation: ""
1287            });
1288            for (var i = 0; i < tracks.length; i++) {
1289                tracks[i].track = tracks[i].track || "";
1290                tracks[i].album = tracks[i].album || "";
1291                tracks[i].artist = tracks[i].artist || "";
1292                tracks[i].artistDisambiguation = tracks[i].artistDisambiguation || "";
1293                tracks[i].albumArtist = tracks[i].albumArtist || "";
1294                tracks[i].albumArtistDisambiguation = tracks[i].albumArtistDisambiguation || "";
1295                (function (track) {
1296                    t.sqlInsert("artists", {
1297                        artist: track.artist,
1298                        artistDisambiguation: track.artistDisambiguation
1299                    });
1300                    t.sqlInsert("albumArtists", {
1301                        albumArtist: track.albumArtist,
1302                        albumArtistDisambiguation: track.albumArtistDisambiguation
1303                    });
1304                })(tracks[i]);
1305            }
1306            return t.execDeferredStatements();
1307        }).then(function () {
1308            // Get all artists' and albumArtists' db ids
1309            t.sqlSelect("albumArtists", function (r) {
1310                return {
1311                    albumArtist: r.albumArtist,
1312                    albumArtistDisambiguation: r.albumArtistDisambiguation,
1313                    _id: r._id
1314                };
1315            });
1316            t.sqlSelect("artists", function (r) {
1317                return {
1318                    artist: r.artist,
1319                    artistDisambiguation: r.artistDisambiguation,
1320                    _id: r._id
1321                };
1322            });
1323            return t.execDeferredStatements();
1324        }).then(function (resultsArray) {
1325            // Store the db ids in a map
1326            var i, row, albumArtists = {};
1327            for (i = 0; i < resultsArray[0].length; i++) {
1328                row = resultsArray[0][i];
1329                albumArtists[row.albumArtist + "♣" + row.albumArtistDisambiguation] = row._id;
1330            }
1331            for (i = 0; i < resultsArray[1].length; i++) {
1332                row = resultsArray[1][i];
1333                cachedArtists[row.artist + "♣" + row.artistDisambiguation] = row._id;
1334                cachedArtistIds[row._id] = {
1335                    artist: row.artist,
1336                    artistDisambiguation: row.artistDisambiguation
1337                };
1338            }
1339
1340            for (i = 0; i < tracks.length; i++) {
1341                var track = tracks[i];
1342                var album = cachedAlbumArtists[track.album];
1343                if (!album) {
1344                    album = cachedAlbumArtists[track.album] = {
1345                        artists: {}
1346                    };
1347                }
1348                album.artists[track.artist] = true;
1349                var artistCount = Object.keys(album.artists).length;
1350                if (artistCount == 1) {
1351                    album.albumArtistId =
1352                        cachedArtists[track.artist + "♣" + track.artistDisambiguation];
1353                } else if (artistCount == 2) {
1354                    album.albumArtistId = cachedArtists["Various Artists♣"];
1355                }
1356            }
1357        }).then(function () {
1358            // Insert all albums
1359            for (var i = 0; i < tracks.length; i++) {
1360                (function (track) {
1361                    var albumArtistId = cachedAlbumArtists[track.album].albumArtistId;
1362                    t.sqlInsert("albums", {
1363                        album: track.album,
1364                        albumArtistId: albumArtistId
1365                    });
1366                })(tracks[i]);
1367            }
1368            return t.execDeferredStatements();
1369        }).then(function () {
1370            // Get the albums' db ids
1371            t.sqlSelect("albums", function (r) {
1372                return {
1373                    album: r.album,
1374                    albumArtistId: r.albumArtistId,
1375                    _id: r._id
1376                };
1377            });
1378            return t

Large files files are truncated, but you can click here to view the full file