<NME> Sitelist.js
<BEF> "use strict";
(function() {
  var _ = require("lodash"),
    URL = require("urlutils");

  // The _internal_ version of the objects in localstorage
  var STORAGE_VERSION = 1;

  /**
   * @return {RegExp} a regex that matches where the string is in a url's (domain) name
   */
  var URLCheck = function(domain, opts) {
    opts = opts || {};

    var inner =
      opts.alias
        ? domain + "|www." + domain + "|" +  _.map(opts.alias,  _.escapeRegExp).join("|")
        : domain + "|www." + domain;

    // [A-Za-z0-9-] should capture all valid characters in a (sub)domain
    // The {0, 3} matching group is there to match up to 3 subdomains
    var re = new RegExp("^(http|https)://(?:[A-Za-z0-9-]*\\.){0,3}(?:" + inner + ")(\\.|:|$)+");

    if(opts.blacklist) {
      var blacklistRe = new RegExp("(" + opts.blacklist.join("|") + ")");

      // XXX: All URL checks (should) make a call to `.test` on the RegExp
      // Here we override it with a custom function to account for blacklisted hostnames
      return {
        test: function(_url) {
          var parsedUrl = new URL(_url);
          return (re.test(_url) && !blacklistRe.test(parsedUrl.host));
        }
      };
    }

    return re;
  };

  /**
   * Base class for all sites enabled in extension
   */
  function Sitelist(sites) {
    this.validSiteAttributes = [
      "name", "url", "controller", "alias", "blacklist"
    ];

    this.sites = sites || {
      "7digital": { name: "7digital", url: "http://www.7digital.com" },
      "8tracks": { name: "8tracks", url: "http://www.8tracks.com" },
      "amazon": { name: "Amazon Cloud Player", url: "https://www.amazon.com/gp/dmusic/cloudplayer/player" },
      "ampache": { name: "Ampache", url: "http://ampache.org" },
      "accuradio": { name: "Accuradio", url: "https://www.accuradio.com" },
      "airsonic": { name: "AirSonic", url: "https://airsonic.github.io/"},
      "ambientsleepingpill": { name: "Ambient Sleeping Pill", url: "http://www.ambientsleepingpill.com" },
      "anghami": { name: "Anghami", url: "https://www.anghami.com" },
      "apple": { name: "Apple Music", url: "https://beta.music.apple.com", controller: "AppleMusicController.js" },
      "arena": { name: "Are.na", url: "https://www.are.na/", alias: ["are"] },
      "asoftmurmur": { name: "A Soft Murmur", url: "http://www.asoftmurmur.com" },
      "audible": { name: "Audible", url: "http://www.audible.com" },
      "audiotool": { name: "Audiotool", url: "https://www.audiotool.com/" },
      "bandcamp": { name: "Bandcamp", url: "http://www.bandcamp.com" },
      "bbc": { name: "BBC Radio", url: "http://www.bbc.co.uk/radio", controller: "BBCRadioController.js" },
      "be-at": { name: "BE-AT.TV", url: "https://be-at.tv", controller: "BeAtTvController.js"},
      "beatport": { name: "Beatport", url: "https://www.beatport.com" },
      "brain": { name: "BrainFM", url: "http://brain.fm", controller: "BrainFMController.js" },
      "castbox": { name: "Castbox", url: "https://castbox.fm", controller: "CastboxController.js" },
      "cloud.caster": { name: "Cloud Caster", url: "http://cloud-caster.com", controller: "CloudCasterController.js", alias: ["cloud-caster"] },
      "coderadio": { name: "Code Radio", url: "https://coderadio.freecodecamp.org/", controller: "CodeRadioController.js" },
      "coursera": { name: "Coursera", url: "http://www.coursera.org" },
      "crave": { name: "Crave", url: "https://www.crave.ca", controller: "CraveController.js" },
      "deezer": { name: "Deezer", url: "http://www.deezer.com" },
      "demodrop": { name: "DemoDrop", url: "http://www.demodrop.com" },
      "di": { name: "Di.fm", url: "http://www.di.fm" },
      "disco": { name: "Disco.io", url: "http://www.disco.io" },
      "disi": { name: "Disi", url: "http://www.disi.co.il" },
      "driveplayer": { name: "Drive Player", url: "http://www.driveplayer.com" },
      "duckburgradio": { name: "Duckburg Radio", url: "http://beta.radio-mb.com", controller: "DuckburgRadioController.js", alias: ["radio-mb"] },
      "earbits": { name: "Earbits", url: "http://www.earbits.com" },
      "egghead": { name: "egghead.io", url: "https://egghead.io" },
      "embedded": { name: "Embedded Element", url: null, controller: "EmbeddedElementController.js" },
      "emby": { name: "Emby", url: "http://app.emby.media" },
      "feedly": { name: "Feedly", url: "http://www.feedly.com" },
      "focusatwill": { name: "focusatwill", url: "http://www.focusatwill.com", controller: "FocusatwillController.js" },
      "focusmusic": { name: "Focusmusic", url: "https://focusmusic.fm/" },
      "friskyradio": { name: "Frisky Radio", url: "https://www.friskyradio.com" },
      "gaana": { name: "Gaana", url: "http://www.gaana.com" },
      "giantbomb": { name: "Giantbomb", url: "http://giantbomb.com" },
      "phenopod": { name: "Phenopod", url: "https://phenopod.com" },
      "play.google": { name: "Google Play Music", url: "http://play.google.com", controller: "GoogleMusicController.js" },
      "podcasts.google": { name: "Google Podcasts", url: "http://podcasts.google.com", controller: "GooglePodcastsController.js" },
      "playmoss": { name: "Playmoss", url: "http://www.playmoss.com" },
      "hearthis": { name: "HearThis.at", url: "http://www.hearthis.at" },
      "hoopla": { name: "Hoopla", url: "https://www.hoopladigital.com", alias:["hoopladigital"] },
      "hulu": { name: "Hulu", url: "https://www.hulu.com" },
      "hypem": { name: "Hypemachine", url: "http://www.hypem.com" },
      "hypster": { name: "Hypster", url: "http://www.hypster.com" },
      "ibroadcast": { name: "iBroadcast", url: "https://media.ibroadcast.com/" },
      "ivoox": { name: "ivoox", url: "http://www.ivoox.com" },
      "jamendo": { name: "Jamendo", url: "https://www.jamendo.com" },
      "jango": { name: "Jango", url: "http://www.jango.com" },
      "kollekt": { name: "Kollekt.fm", url: "http://www.kollekt.fm" },
      "last": { name: "LastFm", url: "http://www.last.fm", controller: "LastfmController.js", alias: ["lastfm"] },
      "listenonrepeat": { name: "ListenOnRepeat", url: "http://www.listenonrepeat.com" },
      "koel": { name: "Koel", url: "https://koel.phanan.net" },
      "kollekt": { name: "Kollekt.fm", url: "http://www.kollekt.fm" },
      "last": { name: "LastFm", url: "http://www.last.fm", controller: "LastfmController.js", alias: ["lastfm"] },
      "lifestore-flat": { name: "Hofer life music", url: "https://app.lifestore-flat.at", controller: "HoferLifeMusicController.js" },
      "listenonrepeat": { name: "ListenOnRepeat", url: "http://www.listenonrepeat.com" },
      "logitechmediaserver": { name: "LogitechMediaServer", url: "http://mysqueezebox.com", controller: "LogitechMediaServerController.js", alias: ["mysqueezebox"] },
      "mixcloud": { name: "Mixcloud", url: "http://www.mixcloud.com" },
      "music.163": { name: "music.163", url: "http://music.163.com", controller: "163Controller.js"},
      "music.microsoft": { name: "Microsoft Groove", url: "http://music.microsoft.com", controller: "MicrosoftController.js" },
      "music.zacharyseguin": { name: "Zachary Seguin Music", "url": "https://music.zacharyseguin.ca", controller: "MusicKitJsController.js" },
      "musicforprogramming": { name: "Music for Programming", url: "https://musicforprogramming.net", controller: "MusicForProgrammingController.js" },
      "mycloudplayers": { name: "My Cloud Player", url: "http://www.mycloudplayers.com" },
      "mynoise" : { name: "myNoise", url : "https://mynoise.net" },
      "myspace": { name: "MySpace", url: "http://www.myspace.com" },
      "music.naver": { name: "Naver Music", url: "https://playerui.music.naver.com", controller: "NaverMusicController.js"},
      "napster": { name: "Napster", url: "https://app.napster.com", controller: "NapsterController.js" },
      "netflix": { name: "Netflix", url: "http://www.netflix.com" },
      "neverthink": { name: "Neverthink", url: "https://neverthink.tv", controller: "NeverthinkController.js" },
      "noise": { name: "NoiseSupply", url: "http://noise.supply", controller: "NoiseSupplyController.js" },
      "noisli": { name: "Noisli", url: "http://noisli.com", controller: "NoisliController.js" },
      "noonpacific": { name: "Noon Pacific", url: "http://www.noonpacific.com" },
      "www.npr": { name: "NPR Player", url: "http://www.npr.org", controller: "NprNewsController.js" },
      "overcast": { name: "Overcast.fm", url: "http://overcast.fm" },
      "palcomp3": { name: "Palco MP3", url: "http://palcomp3.com.br" },
      "pandora": { name: "Pandora", url: "http://www.pandora.com" },
      "patari": { name: "Patari", url: "http://patari.pk", alias: ["patari"] },
      "phishjustjams": { name: "Phish Just Jams", url: "http://phishjustjams.com" },
      "player": { name: "Player.fm", url: "http://player.fm", controller: "PlayerFmController.js", blacklist: ["player.spotify", "reddit.music.player.il", "reddit.musicplayer.io", "player.siriusxm.com"] },
      "pleer": { name: "Pleer", url: "http://pleer.com" },
      "plex": { name: "Plex", url: "http://www.plex.tv" },
      "pluralsight": { name: "Pluralsight", url: "https://app.pluralsight.com" },
      "pocketcasts": { name: "Pocketcasts", url: "https://play.pocketcasts.com" },
      "podster": { name: "Podster", url: "http://www.podster.fm" },
      "pogoplug": { name: "Pogoplug", url: "http://my.pogoplug.com/view" },
      "poolside": { name: "Poolside FM", url: "https://poolside.fm/", controller: "PoolsideFM.js", alias: ["poolsidefm"] },
      "primephonic": {name: "Primephonic", url: "https://www.primephonic.com/"},
      "primevideo": {name: "Prime Video", url: "https://www.primevideo.com"},
      "promodj": {name: "Promo DJ", url: "https://www.promodj.com"},
      "qobuz": { name: "Qobuz", url: "https://play.qobuz.com" },
      "music.qq": { name: "QQ Music", url: "https://y.qq.com/portal/player.html", controller: "QQController.js", alias: ["y.qq.com/portal/player.html"] },
      "radioparadise": { name: "RadioParadise", url: "http://www.radioparadise.com" },
      "radioswissjazz": { name: "RadioSwissJazz", url: "http://www.radioswissjazz.ch" },
      "rainwave": { name: "Rainwave.cc", url: "http://www.rainwave.cc" },
      "reddit.music.player.il": { name: "Reddit Music Player", url: "http://reddit.music.player.il.ly", controller: "RedditMusicPlayerController.js", alias: ["reddit.musicplayer"] },
      "reverbnation": { name: "Reverb Nation", url: "http://www.reverbnation.com" },
      "jiosaavn": { name: "JioSaavn", url: "https://www.jiosaavn.com", controller: "JioSaavnController.js" },
      "seesu": { name: "Seesu.me", url: "http://www.seesu.me" },
      "shortorange": { name: "ShortOrange", url: "http://www.shortorange.com" },
      "siriusxm": { name: "SiriusXM", url: "https://player.siriusxm.com" },
      "livexlive": { name: "LiveXLive", url: "http://livexlive.com" },
      "somafm": { name: "SomaFM", url: "http://somafm.com", controller: "SomaFMController.js" },
      "songstr": { name: "Songstr", url: "http://www.songstr.com" },
      "songza": { name: "Songza", url: "http://www.songza.com" },
      "music.sonyentertainmentnetwork": { name: "Sony Music Unlimited", url: "https://music.sonyentertainmentnetwork.com", controller: "SonyMusicUnlimitedController.js" },
      "sound": { name: "Sound.is", url: "http://www.sound.is" },
      "soundcloud": { name: "Soundcloud", url: "http://www.soundcloud.com" },
      "soundredux": { name: "SoundRedux", url: "https://soundredux.io" },
      "soundsgood": { name: "Soundsgood.co", url: "http://www.soundsgood.co" },
      "spotify": { name: "Spotify Web Player", url: "http://www.spotify.com" },
      "spreaker": { name: "Spreaker", url: "http://www.spreaker.com" },
      "stitcher": { name: "Stitcher", url: "http://www.stitcher.com" },
      "streamelements": { name: "StreamElements", url: "https://streamelements.com/dashboard/songrequest/general" },
      "streamsquid": { name: "StreamSquid", url: "http://streamsquid.com/" },
      "subsonic": { name: "Subsonic", url: "http://www.subsonic.org" },
      "synology": { name: "Synology Audio Station", url: "https://www.synology.me", controller: "SynologyAudioStationController.js" },
      "telegram": { name: "Telegram Web", url: "https://evgeny-nadymov.github.io/telegram-react/", controller: "TelegramReactController.js", alias: ["evgeny-nadymov", "telegram-react"] },
      "tidal": { name: "Tidal", url: "https://www.tidal.com", alias: ["tidalhifi"] },
      "tunein": { name: "TuneIn", url: "http://www.tunein.com" },
      "twitch": { name: "Twitch.tv", url: "http://www.twitch.tv" },
      "udemy": { name: "Udemy", url: "https://www.udemy.com/" },
      "vibe": { name: "Vibe", url: "https://vibe.naver.com"},
      "vimeo": { name: "Vimeo", url: "https://vimeo.com" },
      "vk": { name: "Vkontakte", url: "http://www.vk.com" },
      "wynk": { name: "Wynk", url: "https://www.wynk.in/" },
      "music.yandex": { name: "Yandex", url: "http://music.yandex.ru", controller: "YandexController.js" },
      "radio.yandex": { name: "Yandex Radio", url: "http://radio.yandex.ru", controller: "YandexRadioController.js" },
      "radio": { name: "RadioGarden", url: "http://radio.garden", alias: ["radiogarden"], controller: "RadioGardenController.js" },
      "youarelistening": { name: "YouAreListening.to", url: "http://www.youarelistening.to", controller: "YouarelisteningtoController.js" },
      "xiami": { name: "Xiami", url: "http://www.xiami.com" },
      "music.youtube": { name: "YouTube Music", url: "https://music.youtube.com", controller: "YoutubeMusicController.js" },
      "youtube": { name: "YouTube", url: "http://www.youtube.com" },
      "youzeek": { name: "YOUZEEK", url: "http://www.youzeek.com" },
      "zenva": { name: "Zenva", url: "https://academy.zenva.com/" },
      "zonga": { name: "Zonga", url: "http://asculta.zonga.ro", controller: "ZongaController.js" }
    };

    this.disabledTabs = [];
  }

  /**
   * Get site enabled settings from localstorage
   */
  Sitelist.prototype.loadSettings = function() {
    var that = this;

    chrome.storage.sync.get(function(obj) {
      var objSet = _.has(obj, "hotkey-sites"),
        storageObj = {};

      // Migrate old storage versions to new format
      var version = (typeof obj["hotkey-storage-version"] === "undefined") ? 0 : obj["hotkey-storage-version"];

      _.forEach(_.keys(that.sites), function(siteKey) {
        var siteDefaults = { enabled: true, priority: 1, alias: [], showNotifications: false };
        var siteObj =
          (version === 0)
            ? {
              enabled: objSet ? obj["hotkey-sites"][siteKey] || false : true,
              priority: 1,
              alias: [],
              showNotifications: false
            }
            : (objSet && obj["hotkey-sites"][siteKey])
              // Validate enabled/priority values in case of migration problems
              ? {
                enabled: _.isBoolean(obj["hotkey-sites"][siteKey].enabled) ? obj["hotkey-sites"][siteKey].enabled : true,
                priority: _.isNumber(obj["hotkey-sites"][siteKey].priority) ? obj["hotkey-sites"][siteKey].priority : 1,
                alias: _.isArray(obj["hotkey-sites"][siteKey].alias) ? obj["hotkey-sites"][siteKey].alias : [],
                showNotifications: _.isBoolean(obj["hotkey-sites"][siteKey].showNotifications) ? obj["hotkey-sites"][siteKey].showNotifications : false
              }
              : siteDefaults;

        that.addSite(
          siteKey,
          siteObj
        );

        // Delete properties that have default values to reduce storage space
        // For `chrome.storage.sync` API each item must be under 8kb
        _.forEach(_.keys(siteDefaults), function(property) {
          if(_.isEqual(siteObj[property], siteDefaults[property])) {
            delete siteObj[property];
          }
        });

        storageObj[siteKey] = siteObj;
      });

      // Set the storage key on init incase previous storage format becomes broken
      chrome.storage.sync.set({ "hotkey-sites": storageObj });

      // Set storage version
      chrome.storage.sync.set({ "hotkey-storage-version": STORAGE_VERSION });

      // Initialize popup open on update setting
      if(!Object.prototype.hasOwnProperty.call(obj,"hotkey-open_on_update")) {
        chrome.storage.sync.set({ "hotkey-open_on_update": true });
      }
      if(!Object.prototype.hasOwnProperty.call(obj,"hotkey-use_mpris")) {
        chrome.storage.sync.set({ "hotkey-use_mpris": false });
      }
      if(!Object.prototype.hasOwnProperty.call(obj,"hotkey-youtube_restart")) {
        chrome.storage.sync.set({ "hotkey-youtube_restart": false });
      }
    });
  };

  /**
   * Adds a new site to `sites` and generates the URL regex
   */
  Sitelist.prototype.addSite = function(siteKey, attributes) {
    var site = this.sites[siteKey];

    if(attributes.removedAlias && _.isArray(attributes.removedAlias)) {
      site.alias = _.difference(site.alias, attributes.removedAlias);
    }

    // Combine user defined site aliases with extension defined aliases
    attributes.alias = _.union(site.alias || [], attributes.alias || []);
    attributes.enabled =
      (typeof attributes.enabled === "undefined")
        ? (typeof site.enabled === "undefined")
          ? true
          : site.enabled
        : attributes.enabled;
    attributes.priority =
      (typeof attributes.priority === "undefined")
        ? (typeof site.priority === "undefined")
          ? 1
          : site.priority
        : attributes.priority;
    attributes.showNotifications =
      (typeof attributes.showNotifications === "undefined")
        ? (typeof site.showNotifications === "undefined")
          ? false
          : site.showNotifications
        : attributes.showNotifications;

    this.sites[siteKey] = _.assignIn(
      site,
      _.pickBy(attributes, this.validSiteAttributes),
      {
        enabled: attributes.enabled,
        priority: attributes.priority,
        showNotifications: attributes.showNotifications,
        urlRegex: new URLCheck(siteKey, { alias: attributes.alias, blacklist: site.blacklist })
      }
    );
  };

  /**
   * Set site enabled settings in localstorage
   * @param {String} siteKey - name of the hotkey-sites key in localstorage
   * @param {Object} value - value to set
   * @return {Promise}
   */
  Sitelist.prototype.setSiteStorage = function(siteKey, value) {
    var promise = new Promise(function(resolve, reject) {
      chrome.storage.sync.get(function(obj) {
        if(obj["hotkey-sites"]) {
          _.assignIn(obj["hotkey-sites"][siteKey], value);
          chrome.storage.sync.set({ "hotkey-sites": obj["hotkey-sites"] }, function() {
            resolve(true);
          });
        } else {
          reject("Storage object not found.");
        }
      });
    });

    return promise;
  };

  /**
   * Set the disabled value of a music site and store results in localstorage
   * @param {String} siteName - site name to update
   * @param {Boolean} value - value object to set site to
   * @param {Function} callback
   */
  Sitelist.prototype.setSiteState = function(siteKey, value) {
    var that = this;

    var promise = new Promise(function(resolve) {
      that.addSite(siteKey, value);
      that.setSiteStorage(siteKey, value).then(function() {
        resolve();
      }, function() {
        resolve();
      });
    });

    return promise;
  };

  /**
   * @return {Array} array of enabled site keys
   */
  Sitelist.prototype.getEnabled = function() {
    return _.keys(
      _.pickBy(this.sites, function(site) {
        return site.enabled;
      })
    );
  };

  /**
   * @return {Array} array of showNotifications site keys
   */
  Sitelist.prototype.getShowNotifications = function() {
    return _.keys(
      _.pickBy(this.sites, function(site) {
        return site.showNotifications;
      })
    );
  };

  /**
   * Returns the sitelist key of a url if it is matched to a music site
   * @param {String} url - url to check
   * @return {String} sitelist key if found, null otherwise
   */
  Sitelist.prototype.getSitelistName = function(url) {
    var filtered_sites = _.filter(_.keys(this.sites), (function (name) {
      return this.sites[name].urlRegex.test(url);
    }).bind(this));

    if (!filtered_sites.length) return null;

    return filtered_sites[0];
  };

  /**
   * Gets all of the tabId's of a music site
   * @return {Promise}
   */
  Sitelist.prototype.getMusicTabsByUrl = function(url) {
    var sitelist_name = this.getSitelistName(url),
      that = this;

    var promise = new Promise(function(resolve, reject) {
      if(sitelist_name === null) reject([]);

      var tab_ids = [];
      var urlRegex = that.sites[sitelist_name].urlRegex;
      chrome.tabs.query({}, function(tabs) {
        tabs.forEach(function(tab) {
          if(urlRegex.test(tab.url)) tab_ids.push(tab.id);
        }, this);
        resolve(tab_ids);
      });
    });

    return promise;
  };

  /**
   * @param url {String} url of site to check for
   * @return {Boolean} true if url matches an enabled site
   */
  Sitelist.prototype.checkEnabled = function(url) {
    var _sites = this.sites;

    return this.getEnabled().some(function(sitename) {
      return (_sites[sitename].urlRegex.test(url));
    });
  };

  /**
   * @param url {String} url of site to check for
   * @return {Boolean} true if url matches a showNotifications site
   */
  Sitelist.prototype.checkShowNotifications = function(url) {
    var _sites = this.sites;

    return this.getShowNotifications().some(function(sitename) {
      return (_sites[sitename].urlRegex.test(url));
    });
  };

  /**
   * Checks if a tab has been temp disabled
   * @param {Number} tabId - id of tab to check
   * @return {Boolean} true if tab is enabled
   */
  Sitelist.prototype.checkTabEnabled = function(tabId) {
    return (tabId && this.disabledTabs.indexOf(tabId) === -1);
  };

  /**
   * @param {String} url - url of site to check for
   * @return {Boolean} true if url matches a music site
   */
  Sitelist.prototype.checkMusicSite = function(url) {
    var sitesRegex = _.map(this.sites, function(site) { return site.urlRegex; });

    return sitesRegex.some(function(urlRegex) {
      return (urlRegex.test(url));
    });
  };

  /**
   * @param {Number} tabId - id of tab to temp disable
   * @param {Boolean} enabled - enable tab if true, disable tab if false
   */
  Sitelist.prototype.markTabEnabledState = function(tabId, enabled) {
    tabId = parseInt(tabId);
    if(enabled) {
      this.disabledTabs = this.disabledTabs.filter(function(tab) { return tab !== tabId; });
    } else {
      if(this.disabledTabs.indexOf(tabId) == -1) this.disabledTabs.push(parseInt(tabId));
    }
  };

  /**
   * Gets the filename of a sites controller
   * @param {String} url - URL to get controller for
   * @return {String} controller filename if found
   */
  Sitelist.prototype.getController = function(url) {
    var siteName = this.getSitelistName(url);

    if(siteName) {
      var site = this.sites[siteName];
      if(site.controller) return site.controller;

      return (siteName[0].toUpperCase() + siteName.slice(1) + "Controller.js");
    }

    return null;
  };

  /**
   * @param {String} siteKey
   * @return {Number} priorty of site
   */
  Sitelist.prototype.getPriority = function(siteKey) {
    return this.sites[siteKey].priority;
  };

  /**
   * Gets an array of all tabs that are music tabs, ignoring whether they are active
   * @return {Promise}
   */
  Sitelist.prototype.getMusicTabs = function() {
    var that = this;

    var promise = new Promise(function(resolve) {
      var musicTabs = {
        enabled: [],
        disabled: []
      };

      chrome.tabs.query({}, function (tabs) {
        tabs.forEach(function (tab) {
          if(that.checkEnabled(tab.url)) {
            tab.streamkeysSiteKey = that.getSitelistName(tab.url);
            tab.streamkeysPriority = that.getPriority(tab.streamkeysSiteKey);
            tab.streamkeysEnabled = that.checkTabEnabled(tab.id);
            musicTabs.enabled.push(tab);
          } else if(that.checkMusicSite(tab.url)) {
            tab.streamkeysSiteKey = that.getSitelistName(tab.url);
            tab.streamkeysPriority = that.getPriority(tab.streamkeysSiteKey);
            tab.streamkeysEnabled = false;
            musicTabs.disabled.push(tab);
          }
        });

        resolve(musicTabs);
      });
    });

    return promise;
  };

  /**
   * Gets an array of all active and enabled music tabs
   * @return {Promise}
   */
  Sitelist.prototype.getActiveMusicTabs = function() {
    var that = this;

    var promise = new Promise(function(resolve) {
      var musicTabs = [];

      chrome.tabs.query({}, function (tabs) {
        tabs.forEach(function (tab) {
          if(that.checkEnabled(tab.url) && that.checkTabEnabled(tab.id)) {
            musicTabs.push({
              tab: tab,
              priority: that.getPriority(that.getSitelistName(tab.url))
            });
          }
        });

        if(musicTabs.length > 0) {
          var maxPriority = _.sortBy(musicTabs, function(tab) { return tab.priority * -1; })[0].priority;

          musicTabs = _.map(
            _.filter(
              musicTabs,
              function(tab) { return tab.priority == maxPriority; }
            ),
            function(musicTab) { return musicTab.tab; }
          );
        }

        resolve(musicTabs);
      });
    });

    return promise;
  };

  module.exports = Sitelist;
})();

<MSG> Merge pull request #312 from shuuji3/master

Add Koel controller
<DFF> @@ -99,6 +99,7 @@
       "ivoox": { name: "ivoox", url: "http://www.ivoox.com" },
       "jamendo": { name: "Jamendo", url: "https://www.jamendo.com" },
       "jango": { name: "Jango", url: "http://www.jango.com" },
+      "koel": { name: "Koel", url: "https://koel.phanan.net" },
       "kollekt": { name: "Kollekt.fm", url: "http://www.kollekt.fm" },
       "last": { name: "LastFm", url: "http://www.last.fm", controller: "LastfmController.js", alias: ["lastfm"] },
       "listenonrepeat": { name: "ListenOnRepeat", url: "http://www.listenonrepeat.com" },

{'addition_count': 1, 'commit_subject': 'Merge pull request #312 from shuuji3/master', 'deletion_count': 0, 'file_extension': '.js', 'lang': 'js', 'license': 'mit', 'repo_name': 'berrberr/streamkeys'}