/* Minification failed. Returning unminified contents.
(3713,29): run-time error JS1004: Expected ';'
(3713,39-40): run-time error JS1010: Expected identifier: (
(3716,27): run-time error JS1004: Expected ';'
(3721,40): run-time error JS1004: Expected ';'
(3721,50-51): run-time error JS1010: Expected identifier: (
(3723,25): run-time error JS1004: Expected ';'
(8839,73-74): run-time error JS1009: Expected '}': [
(8839,76-77): run-time error JS1006: Expected ')': :
(8839,76-77): run-time error JS1006: Expected ')': :
(8839,78-79): run-time error JS1006: Expected ')': v
(8839,81-82): run-time error JS1195: Expected expression: )
(8839,82-83): run-time error JS1195: Expected expression: )
(8839,83-84): run-time error JS1195: Expected expression: )
(8922,6-7): run-time error JS1006: Expected ')': ;
(9166,1-2): run-time error JS1002: Syntax error: }
(9166,2-3): run-time error JS1195: Expected expression: )
(9118,5,9163,6): run-time error JS1018: 'return' statement outside of function: return {

        GetType: function () {
            return getType();
        },

        GetFilePath: getFilePath,

        Get: get,

        SendMessage: sendMessage,

        ReceiveMessage: receiveMessage,

        // Enable code below to pass messages from iFrames to background script and then to the main page. Search the project for IFRAMECOM.
        //DelayedGet: delayedGet,

        GetExtensionVersion: function () {
            return getExtensionVersion();
        },

        SetExtensionVersion: function (version) {
            extenstionVersion = version;
        },

        Post: post,

        SetDbValue: function (key, value, eventName) {

            setDbValue(key, value, eventName);

        },

        GetDbValue: function (key, eventName) {

            getDbValue(key, eventName);

        },

        RemoveDbValue: function (key) {

            removeDbValue(key);

        },

    }
 */
/*! @license DOMPurify | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.0.8/LICENSE */

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
            (global = global || self, global.DOMPurify = factory());
}(this, function () {
    'use strict';

    function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

    var hasOwnProperty = Object.hasOwnProperty,
        setPrototypeOf = Object.setPrototypeOf,
        isFrozen = Object.isFrozen;
    var freeze = Object.freeze,
        seal = Object.seal,
        create = Object.create; // eslint-disable-line import/no-mutable-exports

    var _ref = typeof Reflect !== 'undefined' && Reflect,
        apply = _ref.apply,
        construct = _ref.construct;

    if (!apply) {
        apply = function apply(fun, thisValue, args) {
            return fun.apply(thisValue, args);
        };
    }

    if (!freeze) {
        freeze = function freeze(x) {
            return x;
        };
    }

    if (!seal) {
        seal = function seal(x) {
            return x;
        };
    }

    if (!construct) {
        construct = function construct(Func, args) {
            return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))();
        };
    }

    var arrayForEach = unapply(Array.prototype.forEach);
    var arrayPop = unapply(Array.prototype.pop);
    var arrayPush = unapply(Array.prototype.push);

    var stringToLowerCase = unapply(String.prototype.toLowerCase);
    var stringMatch = unapply(String.prototype.match);
    var stringReplace = unapply(String.prototype.replace);
    var stringIndexOf = unapply(String.prototype.indexOf);
    var stringTrim = unapply(String.prototype.trim);

    var regExpTest = unapply(RegExp.prototype.test);

    var typeErrorCreate = unconstruct(TypeError);

    function unapply(func) {
        return function (thisArg) {
            for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
                args[_key - 1] = arguments[_key];
            }

            return apply(func, thisArg, args);
        };
    }

    function unconstruct(func) {
        return function () {
            for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
                args[_key2] = arguments[_key2];
            }

            return construct(func, args);
        };
    }

    /* Add properties to a lookup table */
    function addToSet(set, array) {
        if (setPrototypeOf) {
            // Make 'in' and truthy checks like Boolean(set.constructor)
            // independent of any properties defined on Object.prototype.
            // Prevent prototype setters from intercepting set as a this value.
            setPrototypeOf(set, null);
        }

        var l = array.length;
        while (l--) {
            var element = array[l];
            if (typeof element === 'string') {
                var lcElement = stringToLowerCase(element);
                if (lcElement !== element) {
                    // Config presets (e.g. tags.js, attrs.js) are immutable.
                    if (!isFrozen(array)) {
                        array[l] = lcElement;
                    }

                    element = lcElement;
                }
            }

            set[element] = true;
        }

        return set;
    }

    /* Shallow clone an object */
    function clone(object) {
        var newObject = create(null);

        var property = void 0;
        for (property in object) {
            if (apply(hasOwnProperty, object, [property])) {
                newObject[property] = object[property];
            }
        }

        return newObject;
    }

    var html = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);

    // SVG
    var svg = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'audio', 'canvas', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'video', 'view', 'vkern']);

    var svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);

    var mathMl = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover']);

    var text = freeze(['#text']);

    var html$1 = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns']);

    var svg$1 = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);

    var mathMl$1 = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);

    var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);

    // eslint-disable-next-line unicorn/better-regex
    var MUSTACHE_EXPR = seal(/\{\{[\s\S]*|[\s\S]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
    var ERB_EXPR = seal(/<%[\s\S]*|[\s\S]*%>/gm);
    var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
    var ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
    var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
    );
    var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
    var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
    );

    var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

    function _toConsumableArray$1(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

    var getGlobal = function getGlobal() {
        return typeof window === 'undefined' ? null : window;
    };

    /**
     * Creates a no-op policy for internal use only.
     * Don't export this function outside this module!
     * @param {?TrustedTypePolicyFactory} trustedTypes The policy factory.
     * @param {Document} document The document object (to determine policy name suffix)
     * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types
     * are not supported).
     */
    var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) {
        if ((typeof trustedTypes === 'undefined' ? 'undefined' : _typeof(trustedTypes)) !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
            return null;
        }

        // Allow the callers to control the unique policy name
        // by adding a data-tt-policy-suffix to the script element with the DOMPurify.
        // Policy creation with duplicate names throws in Trusted Types.
        var suffix = null;
        var ATTR_NAME = 'data-tt-policy-suffix';
        if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) {
            suffix = document.currentScript.getAttribute(ATTR_NAME);
        }

        var policyName = 'dompurify' + (suffix ? '#' + suffix : '');

        try {
            return trustedTypes.createPolicy(policyName, {
                createHTML: function createHTML(html$$1) {
                    return html$$1;
                }
            });
        } catch (_) {
            // Policy creation failed (most likely another DOMPurify script has
            // already run). Skip creating the policy, as this will only cause errors
            // if TT are enforced.
            console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
            return null;
        }
    };

    function createDOMPurify() {
        var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();

        var DOMPurify = function DOMPurify(root) {
            return createDOMPurify(root);
        };

        /**
         * Version label, exposed for easier checks
         * if DOMPurify is up to date or not
         */
        DOMPurify.version = '2.1.1';

        /**
         * Array of elements that DOMPurify removed during sanitation.
         * Empty if nothing was removed.
         */
        DOMPurify.removed = [];

        if (!window || !window.document || window.document.nodeType !== 9) {
            // Not running in a browser, provide a factory function
            // so that you can pass your own Window
            DOMPurify.isSupported = false;

            return DOMPurify;
        }

        var originalDocument = window.document;

        var document = window.document;
        var DocumentFragment = window.DocumentFragment,
            HTMLTemplateElement = window.HTMLTemplateElement,
            Node = window.Node,
            NodeFilter = window.NodeFilter,
            _window$NamedNodeMap = window.NamedNodeMap,
            NamedNodeMap = _window$NamedNodeMap === undefined ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,
            Text = window.Text,
            Comment = window.Comment,
            DOMParser = window.DOMParser,
            trustedTypes = window.trustedTypes;

        // As per issue #47, the web-components registry is inherited by a
        // new document created via createHTMLDocument. As per the spec
        // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
        // a new empty registry is used when creating a template contents owner
        // document, so we use that as our parent document to ensure nothing
        // is inherited.

        if (typeof HTMLTemplateElement === 'function') {
            var template = document.createElement('template');
            if (template.content && template.content.ownerDocument) {
                document = template.content.ownerDocument;
            }
        }

        var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument);
        var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML('') : '';

        var _document = document,
            implementation = _document.implementation,
            createNodeIterator = _document.createNodeIterator,
            getElementsByTagName = _document.getElementsByTagName,
            createDocumentFragment = _document.createDocumentFragment;
        var importNode = originalDocument.importNode;


        var documentMode = {};
        try {
            documentMode = clone(document).documentMode ? document.documentMode : {};
        } catch (_) { }

        var hooks = {};

        /**
         * Expose whether this browser supports running the full DOMPurify.
         */
        DOMPurify.isSupported = implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9;

        var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR,
            ERB_EXPR$$1 = ERB_EXPR,
            DATA_ATTR$$1 = DATA_ATTR,
            ARIA_ATTR$$1 = ARIA_ATTR,
            IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA,
            ATTR_WHITESPACE$$1 = ATTR_WHITESPACE;
        var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI;

        /**
         * We consider the elements and attributes below to be safe. Ideally
         * don't add any new ones but feel free to remove unwanted ones.
         */

        /* allowed element names */

        var ALLOWED_TAGS = null;
        var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text)));

        /* Allowed attribute names */
        var ALLOWED_ATTR = null;
        var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml)));

        /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
        var FORBID_TAGS = null;

        /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
        var FORBID_ATTR = null;

        /* Decide if ARIA attributes are okay */
        var ALLOW_ARIA_ATTR = true;

        /* Decide if custom data attributes are okay */
        var ALLOW_DATA_ATTR = true;

        /* Decide if unknown protocols are okay */
        var ALLOW_UNKNOWN_PROTOCOLS = false;

        /* Output should be safe for common template engines.
         * This means, DOMPurify removes data attributes, mustaches and ERB
         */
        var SAFE_FOR_TEMPLATES = false;

        /* Decide if document with <html>... should be returned */
        var WHOLE_DOCUMENT = false;

        /* Track whether config is already set on this instance of DOMPurify. */
        var SET_CONFIG = false;

        /* Decide if all elements (e.g. style, script) must be children of
         * document.body. By default, browsers might move them to document.head */
        var FORCE_BODY = false;

        /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
         * string (or a TrustedHTML object if Trusted Types are supported).
         * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
         */
        var RETURN_DOM = false;

        /* Decide if a DOM `DocumentFragment` should be returned, instead of a html
         * string  (or a TrustedHTML object if Trusted Types are supported) */
        var RETURN_DOM_FRAGMENT = false;

        /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM
         * `Node` is imported into the current `Document`. If this flag is not enabled the
         * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by
         * DOMPurify. */
        var RETURN_DOM_IMPORT = false;

        /* Try to return a Trusted Type object instead of a string, return a string in
         * case Trusted Types are not supported  */
        var RETURN_TRUSTED_TYPE = false;

        /* Output should be free from DOM clobbering attacks? */
        var SANITIZE_DOM = true;

        /* Keep element content when removing element? */
        var KEEP_CONTENT = true;

        /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
         * of importing it into a new Document and returning a sanitized copy */
        var IN_PLACE = false;

        /* Allow usage of profiles like html, svg and mathMl */
        var USE_PROFILES = {};

        /* Tags to ignore content of when KEEP_CONTENT is true */
        var FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);

        /* Tags that are safe for data: URIs */
        var DATA_URI_TAGS = null;
        var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);

        /* Attributes safe for values like "javascript:" */
        var URI_SAFE_ATTRIBUTES = null;
        var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'summary', 'title', 'value', 'style', 'xmlns']);

        /* Keep a reference to config to pass to hooks */
        var CONFIG = null;

        /* Ideally, do not touch anything below this line */
        /* ______________________________________________ */

        var formElement = document.createElement('form');

        /**
         * _parseConfig
         *
         * @param  {Object} cfg optional config literal
         */
        // eslint-disable-next-line complexity
        var _parseConfig = function _parseConfig(cfg) {
            if (CONFIG && CONFIG === cfg) {
                return;
            }

            /* Shield configuration object from tampering */
            if (!cfg || (typeof cfg === 'undefined' ? 'undefined' : _typeof(cfg)) !== 'object') {
                cfg = {};
            }

            /* Shield configuration object from prototype pollution */
            cfg = clone(cfg);

            /* Set configuration parameters */
            ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;
            ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;
            URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES;
            DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS;
            FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS) : {};
            FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR) : {};
            USE_PROFILES = 'USE_PROFILES' in cfg ? cfg.USE_PROFILES : false;
            ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
            ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
            ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
            SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
            WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
            RETURN_DOM = cfg.RETURN_DOM || false; // Default false
            RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
            RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT || false; // Default false
            RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
            FORCE_BODY = cfg.FORCE_BODY || false; // Default false
            SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
            KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
            IN_PLACE = cfg.IN_PLACE || false; // Default false
            IS_ALLOWED_URI$$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$$1;
            if (SAFE_FOR_TEMPLATES) {
                ALLOW_DATA_ATTR = false;
            }

            if (RETURN_DOM_FRAGMENT) {
                RETURN_DOM = true;
            }

            /* Parse profile info */
            if (USE_PROFILES) {
                ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text)));
                ALLOWED_ATTR = [];
                if (USE_PROFILES.html === true) {
                    addToSet(ALLOWED_TAGS, html);
                    addToSet(ALLOWED_ATTR, html$1);
                }

                if (USE_PROFILES.svg === true) {
                    addToSet(ALLOWED_TAGS, svg);
                    addToSet(ALLOWED_ATTR, svg$1);
                    addToSet(ALLOWED_ATTR, xml);
                }

                if (USE_PROFILES.svgFilters === true) {
                    addToSet(ALLOWED_TAGS, svgFilters);
                    addToSet(ALLOWED_ATTR, svg$1);
                    addToSet(ALLOWED_ATTR, xml);
                }

                if (USE_PROFILES.mathMl === true) {
                    addToSet(ALLOWED_TAGS, mathMl);
                    addToSet(ALLOWED_ATTR, mathMl$1);
                    addToSet(ALLOWED_ATTR, xml);
                }
            }

            /* Merge configuration parameters */
            if (cfg.ADD_TAGS) {
                if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
                    ALLOWED_TAGS = clone(ALLOWED_TAGS);
                }

                addToSet(ALLOWED_TAGS, cfg.ADD_TAGS);
            }

            if (cfg.ADD_ATTR) {
                if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
                    ALLOWED_ATTR = clone(ALLOWED_ATTR);
                }

                addToSet(ALLOWED_ATTR, cfg.ADD_ATTR);
            }

            if (cfg.ADD_URI_SAFE_ATTR) {
                addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR);
            }

            /* Add #text in case KEEP_CONTENT is set to true */
            if (KEEP_CONTENT) {
                ALLOWED_TAGS['#text'] = true;
            }

            /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
            if (WHOLE_DOCUMENT) {
                addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
            }

            /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
            if (ALLOWED_TAGS.table) {
                addToSet(ALLOWED_TAGS, ['tbody']);
                delete FORBID_TAGS.tbody;
            }

            // Prevent further manipulation of configuration.
            // Not available in IE8, Safari 5, etc.
            if (freeze) {
                freeze(cfg);
            }

            CONFIG = cfg;
        };

        /**
         * _forceRemove
         *
         * @param  {Node} node a DOM node
         */
        var _forceRemove = function _forceRemove(node) {
            arrayPush(DOMPurify.removed, { element: node });
            try {
                node.parentNode.removeChild(node);
            } catch (_) {
                node.outerHTML = emptyHTML;
            }
        };

        /**
         * _removeAttribute
         *
         * @param  {String} name an Attribute name
         * @param  {Node} node a DOM node
         */
        var _removeAttribute = function _removeAttribute(name, node) {
            try {
                arrayPush(DOMPurify.removed, {
                    attribute: node.getAttributeNode(name),
                    from: node
                });
            } catch (_) {
                arrayPush(DOMPurify.removed, {
                    attribute: null,
                    from: node
                });
            }

            node.removeAttribute(name);
        };

        /**
         * _initDocument
         *
         * @param  {String} dirty a string of dirty markup
         * @return {Document} a DOM, filled with the dirty markup
         */
        var _initDocument = function _initDocument(dirty) {
            /* Create a HTML document */
            var doc = void 0;
            var leadingWhitespace = void 0;

            if (FORCE_BODY) {
                dirty = '<remove></remove>' + dirty;
            } else {
                /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
                var matches = stringMatch(dirty, /^[\r\n\t ]+/);
                leadingWhitespace = matches && matches[0];
            }

            var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
            /* Use the DOMParser API by default, fallback later if needs be */
            try {
                doc = new DOMParser().parseFromString(dirtyPayload, 'text/html');
            } catch (_) { }

            /* Use createHTMLDocument in case DOMParser is not available */
            if (!doc || !doc.documentElement) {
                doc = implementation.createHTMLDocument('');
                var _doc = doc,
                    body = _doc.body;

                body.parentNode.removeChild(body.parentNode.firstElementChild);
                body.outerHTML = dirtyPayload;
            }

            if (dirty && leadingWhitespace) {
                doc.body.insertBefore(document.createTextNode(leadingWhitespace), doc.body.childNodes[0] || null);
            }

            /* Work on whole document or just its body */
            return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
        };

        /**
         * _createIterator
         *
         * @param  {Document} root document/fragment to create iterator for
         * @return {Iterator} iterator instance
         */
        var _createIterator = function _createIterator(root) {
            return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function () {
                return NodeFilter.FILTER_ACCEPT;
            }, false);
        };

        /**
         * _isClobbered
         *
         * @param  {Node} elm element to check for clobbering attacks
         * @return {Boolean} true if clobbered, false if safe
         */
        var _isClobbered = function _isClobbered(elm) {
            if (elm instanceof Text || elm instanceof Comment) {
                return false;
            }

            if (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string') {
                return true;
            }

            return false;
        };

        /**
         * _isNode
         *
         * @param  {Node} obj object to check whether it's a DOM node
         * @return {Boolean} true is object is a DOM node
         */
        var _isNode = function _isNode(object) {
            return (typeof Node === 'undefined' ? 'undefined' : _typeof(Node)) === 'object' ? object instanceof Node : object && (typeof object === 'undefined' ? 'undefined' : _typeof(object)) === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string';
        };

        /**
         * _executeHook
         * Execute user configurable hooks
         *
         * @param  {String} entryPoint  Name of the hook's entry point
         * @param  {Node} currentNode node to work on with the hook
         * @param  {Object} data additional hook parameters
         */
        var _executeHook = function _executeHook(entryPoint, currentNode, data) {
            if (!hooks[entryPoint]) {
                return;
            }

            arrayForEach(hooks[entryPoint], function (hook) {
                hook.call(DOMPurify, currentNode, data, CONFIG);
            });
        };

        /**
         * _sanitizeElements
         *
         * @protect nodeName
         * @protect textContent
         * @protect removeChild
         *
         * @param   {Node} currentNode to check for permission to exist
         * @return  {Boolean} true if node was killed, false if left alive
         */
        var _sanitizeElements = function _sanitizeElements(currentNode) {
            var content = void 0;

            /* Execute a hook if present */
            _executeHook('beforeSanitizeElements', currentNode, null);

            /* Check if element is clobbered or can clobber */
            if (_isClobbered(currentNode)) {
                _forceRemove(currentNode);
                return true;
            }

            /* Check if tagname contains Unicode */
            if (stringMatch(currentNode.nodeName, /[\u0080-\uFFFF]/)) {
                _forceRemove(currentNode);
                return true;
            }

            /* Now let's check the element's type and name */
            var tagName = stringToLowerCase(currentNode.nodeName);

            /* Execute a hook if present */
            _executeHook('uponSanitizeElement', currentNode, {
                tagName: tagName,
                allowedTags: ALLOWED_TAGS
            });

            /* Take care of an mXSS pattern using p, br inside svg, math */
            if ((tagName === 'svg' || tagName === 'math') && currentNode.querySelectorAll('p, br').length !== 0) {
                _forceRemove(currentNode);
                return true;
            }

            /* Detect mXSS attempts abusing namespace confusion */
            if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[!/\w]/g, currentNode.innerHTML) && regExpTest(/<[!/\w]/g, currentNode.textContent)) {
                _forceRemove(currentNode);
                return true;
            }

            /* Remove element if anything forbids its presence */
            if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
                /* Keep content except for bad-listed elements */
                if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && typeof currentNode.insertAdjacentHTML === 'function') {
                    try {
                        var htmlToInsert = currentNode.innerHTML;
                        currentNode.insertAdjacentHTML('AfterEnd', trustedTypesPolicy ? trustedTypesPolicy.createHTML(htmlToInsert) : htmlToInsert);
                    } catch (_) { }
                }

                _forceRemove(currentNode);
                return true;
            }

            /* Remove in case a noscript/noembed XSS is suspected */
            if ((tagName === 'noscript' || tagName === 'noembed') && regExpTest(/<\/no(script|embed)/i, currentNode.innerHTML)) {
                _forceRemove(currentNode);
                return true;
            }

            /* Sanitize element content to be template-safe */
            if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {
                /* Get the element's text content */
                content = currentNode.textContent;
                content = stringReplace(content, MUSTACHE_EXPR$$1, ' ');
                content = stringReplace(content, ERB_EXPR$$1, ' ');
                if (currentNode.textContent !== content) {
                    arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
                    currentNode.textContent = content;
                }
            }

            /* Execute a hook if present */
            _executeHook('afterSanitizeElements', currentNode, null);

            return false;
        };

        /**
         * _isValidAttribute
         *
         * @param  {string} lcTag Lowercase tag name of containing element.
         * @param  {string} lcName Lowercase attribute name.
         * @param  {string} value Attribute value.
         * @return {Boolean} Returns true if `value` is valid, otherwise false.
         */
        // eslint-disable-next-line complexity
        var _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
            /* Make sure attribute cannot clobber */
            if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
                return false;
            }

            /* Allow valid data-* attributes: At least one character after "-"
                (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
                XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
                We don't need to check the value; it's always URI safe. */
            if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$$1, lcName)); else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$$1, lcName)); else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
                return false;

                /* Check value is safe. First, is attr inert? If so, is safe */
            } else if (URI_SAFE_ATTRIBUTES[lcName]); else if (regExpTest(IS_ALLOWED_URI$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))); else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]); else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))); else if (!value); else {
                return false;
            }

            return true;
        };

        /**
         * _sanitizeAttributes
         *
         * @protect attributes
         * @protect nodeName
         * @protect removeAttribute
         * @protect setAttribute
         *
         * @param  {Node} currentNode to sanitize
         */
        var _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
            var attr = void 0;
            var value = void 0;
            var lcName = void 0;
            var l = void 0;
            /* Execute a hook if present */
            _executeHook('beforeSanitizeAttributes', currentNode, null);

            var attributes = currentNode.attributes;

            /* Check if we have attributes; if not we might have a text node */

            if (!attributes) {
                return;
            }

            var hookEvent = {
                attrName: '',
                attrValue: '',
                keepAttr: true,
                allowedAttributes: ALLOWED_ATTR
            };
            l = attributes.length;

            /* Go backwards over all attributes; safely remove bad ones */
            while (l--) {
                attr = attributes[l];
                var _attr = attr,
                    name = _attr.name,
                    namespaceURI = _attr.namespaceURI;

                value = stringTrim(attr.value);
                lcName = stringToLowerCase(name);

                /* Execute a hook if present */
                hookEvent.attrName = lcName;
                hookEvent.attrValue = value;
                hookEvent.keepAttr = true;
                hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
                _executeHook('uponSanitizeAttribute', currentNode, hookEvent);
                value = hookEvent.attrValue;
                /* Did the hooks approve of the attribute? */
                if (hookEvent.forceKeepAttr) {
                    continue;
                }

                /* Remove attribute */
                _removeAttribute(name, currentNode);

                /* Did the hooks approve of the attribute? */
                if (!hookEvent.keepAttr) {
                    continue;
                }

                /* Work around a security issue in jQuery 3.0 */
                if (regExpTest(/\/>/i, value)) {
                    _removeAttribute(name, currentNode);
                    continue;
                }

                /* Sanitize attribute content to be template-safe */
                if (SAFE_FOR_TEMPLATES) {
                    value = stringReplace(value, MUSTACHE_EXPR$$1, ' ');
                    value = stringReplace(value, ERB_EXPR$$1, ' ');
                }

                /* Is `value` valid for this attribute? */
                var lcTag = currentNode.nodeName.toLowerCase();
                if (!_isValidAttribute(lcTag, lcName, value)) {
                    continue;
                }

                /* Handle invalid data-* attribute set by try-catching it */
                try {
                    if (namespaceURI) {
                        currentNode.setAttributeNS(namespaceURI, name, value);
                    } else {
                        /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
                        currentNode.setAttribute(name, value);
                    }

                    arrayPop(DOMPurify.removed);
                } catch (_) { }
            }

            /* Execute a hook if present */
            _executeHook('afterSanitizeAttributes', currentNode, null);
        };

        /**
         * _sanitizeShadowDOM
         *
         * @param  {DocumentFragment} fragment to iterate over recursively
         */
        var _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
            var shadowNode = void 0;
            var shadowIterator = _createIterator(fragment);

            /* Execute a hook if present */
            _executeHook('beforeSanitizeShadowDOM', fragment, null);

            while (shadowNode = shadowIterator.nextNode()) {
                /* Execute a hook if present */
                _executeHook('uponSanitizeShadowNode', shadowNode, null);

                /* Sanitize tags and elements */
                if (_sanitizeElements(shadowNode)) {
                    continue;
                }

                /* Deep shadow DOM detected */
                if (shadowNode.content instanceof DocumentFragment) {
                    _sanitizeShadowDOM(shadowNode.content);
                }

                /* Check attributes, sanitize if necessary */
                _sanitizeAttributes(shadowNode);
            }

            /* Execute a hook if present */
            _executeHook('afterSanitizeShadowDOM', fragment, null);
        };

        /**
         * Sanitize
         * Public method providing core sanitation functionality
         *
         * @param {String|Node} dirty string or DOM node
         * @param {Object} configuration object
         */
        // eslint-disable-next-line complexity
        DOMPurify.sanitize = function (dirty, cfg) {
            var body = void 0;
            var importedNode = void 0;
            var currentNode = void 0;
            var oldNode = void 0;
            var returnNode = void 0;
            /* Make sure we have a string to sanitize.
              DO NOT return early, as this will return the wrong type if
              the user has requested a DOM object rather than a string */
            if (!dirty) {
                dirty = '<!-->';
            }

            /* Stringify, in case dirty is an object */
            if (typeof dirty !== 'string' && !_isNode(dirty)) {
                // eslint-disable-next-line no-negated-condition
                if (typeof dirty.toString !== 'function') {
                    throw typeErrorCreate('toString is not a function');
                } else {
                    dirty = dirty.toString();
                    if (typeof dirty !== 'string') {
                        throw typeErrorCreate('dirty is not a string, aborting');
                    }
                }
            }

            /* Check we can run. Otherwise fall back or ignore */
            if (!DOMPurify.isSupported) {
                if (_typeof(window.toStaticHTML) === 'object' || typeof window.toStaticHTML === 'function') {
                    if (typeof dirty === 'string') {
                        return window.toStaticHTML(dirty);
                    }

                    if (_isNode(dirty)) {
                        return window.toStaticHTML(dirty.outerHTML);
                    }
                }

                return dirty;
            }

            /* Assign config vars */
            if (!SET_CONFIG) {
                _parseConfig(cfg);
            }

            /* Clean up removed elements */
            DOMPurify.removed = [];

            /* Check if dirty is correctly typed for IN_PLACE */
            if (typeof dirty === 'string') {
                IN_PLACE = false;
            }

            if (IN_PLACE); else if (dirty instanceof Node) {
                /* If dirty is a DOM element, append to an empty document to avoid
                   elements being stripped by the parser */
                body = _initDocument('<!---->');
                importedNode = body.ownerDocument.importNode(dirty, true);
                if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {
                    /* Node is already a body, use as is */
                    body = importedNode;
                } else if (importedNode.nodeName === 'HTML') {
                    body = importedNode;
                } else {
                    // eslint-disable-next-line unicorn/prefer-node-append
                    body.appendChild(importedNode);
                }
            } else {
                /* Exit directly if we have nothing to do */
                if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
                    // eslint-disable-next-line unicorn/prefer-includes
                    dirty.indexOf('<') === -1) {
                    return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
                }

                /* Initialize the document to work on */
                body = _initDocument(dirty);

                /* Check we have a DOM node from the data */
                if (!body) {
                    return RETURN_DOM ? null : emptyHTML;
                }
            }

            /* Remove first element node (ours) if FORCE_BODY is set */
            if (body && FORCE_BODY) {
                _forceRemove(body.firstChild);
            }

            /* Get node iterator */
            var nodeIterator = _createIterator(IN_PLACE ? dirty : body);

            /* Now start iterating over the created document */
            while (currentNode = nodeIterator.nextNode()) {
                /* Fix IE's strange behavior with manipulated textNodes #89 */
                if (currentNode.nodeType === 3 && currentNode === oldNode) {
                    continue;
                }

                /* Sanitize tags and elements */
                if (_sanitizeElements(currentNode)) {
                    continue;
                }

                /* Shadow DOM detected, sanitize it */
                if (currentNode.content instanceof DocumentFragment) {
                    _sanitizeShadowDOM(currentNode.content);
                }

                /* Check attributes, sanitize if necessary */
                _sanitizeAttributes(currentNode);

                oldNode = currentNode;
            }

            oldNode = null;

            /* If we sanitized `dirty` in-place, return it. */
            if (IN_PLACE) {
                return dirty;
            }

            /* Return sanitized string or DOM */
            if (RETURN_DOM) {
                if (RETURN_DOM_FRAGMENT) {
                    returnNode = createDocumentFragment.call(body.ownerDocument);

                    while (body.firstChild) {
                        // eslint-disable-next-line unicorn/prefer-node-append
                        returnNode.appendChild(body.firstChild);
                    }
                } else {
                    returnNode = body;
                }

                if (RETURN_DOM_IMPORT) {
                    /*
                      AdoptNode() is not used because internal state is not reset
                      (e.g. the past names map of a HTMLFormElement), this is safe
                      in theory but we would rather not risk another attack vector.
                      The state that is cloned by importNode() is explicitly defined
                      by the specs.
                    */
                    returnNode = importNode.call(originalDocument, returnNode, true);
                }

                return returnNode;
            }

            var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;

            /* Sanitize final string template-safe */
            if (SAFE_FOR_TEMPLATES) {
                serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, ' ');
                serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, ' ');
            }

            return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
        };

        /**
         * Public method to set the configuration once
         * setConfig
         *
         * @param {Object} cfg configuration object
         */
        DOMPurify.setConfig = function (cfg) {
            _parseConfig(cfg);
            SET_CONFIG = true;
        };

        /**
         * Public method to remove the configuration
         * clearConfig
         *
         */
        DOMPurify.clearConfig = function () {
            CONFIG = null;
            SET_CONFIG = false;
        };

        /**
         * Public method to check if an attribute value is valid.
         * Uses last set config, if any. Otherwise, uses config defaults.
         * isValidAttribute
         *
         * @param  {string} tag Tag name of containing element.
         * @param  {string} attr Attribute name.
         * @param  {string} value Attribute value.
         * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.
         */
        DOMPurify.isValidAttribute = function (tag, attr, value) {
            /* Initialize shared config vars if necessary. */
            if (!CONFIG) {
                _parseConfig({});
            }

            var lcTag = stringToLowerCase(tag);
            var lcName = stringToLowerCase(attr);
            return _isValidAttribute(lcTag, lcName, value);
        };

        /**
         * AddHook
         * Public method to add DOMPurify hooks
         *
         * @param {String} entryPoint entry point for the hook to add
         * @param {Function} hookFunction function to execute
         */
        DOMPurify.addHook = function (entryPoint, hookFunction) {
            if (typeof hookFunction !== 'function') {
                return;
            }

            hooks[entryPoint] = hooks[entryPoint] || [];
            arrayPush(hooks[entryPoint], hookFunction);
        };

        /**
         * RemoveHook
         * Public method to remove a DOMPurify hook at a given entryPoint
         * (pops it from the stack of hooks if more are present)
         *
         * @param {String} entryPoint entry point for the hook to remove
         */
        DOMPurify.removeHook = function (entryPoint) {
            if (hooks[entryPoint]) {
                arrayPop(hooks[entryPoint]);
            }
        };

        /**
         * RemoveHooks
         * Public method to remove all DOMPurify hooks at a given entryPoint
         *
         * @param  {String} entryPoint entry point for the hooks to remove
         */
        DOMPurify.removeHooks = function (entryPoint) {
            if (hooks[entryPoint]) {
                hooks[entryPoint] = [];
            }
        };

        /**
         * RemoveAllHooks
         * Public method to remove all DOMPurify hooks
         *
         */
        DOMPurify.removeAllHooks = function () {
            hooks = {};
        };

        return DOMPurify;
    }

    var purify = createDOMPurify();

    return purify;

}));
//# sourceMappingURL=purify.js.map;
var pluginHost = 'https://www.tubebuddy.com';
var pluginFileRoot = "chrome-extension://EXTENSION_ID/"
var loadLocalHTML = true;

var DOMPurifyConfig = {
    ADD_TAGS: ['meta', 'iframe'],
    ADD_ATTR: ['itemprop', 'content', 'src', 'target'],
    SAFE_FOR_JQUERY: true,
    SANITIZE_DOM: false,
    WHOLE_DOCUMENT: true
};

DOMPurify.addHook('uponSanitizeAttribute', function (
    currentNode,
    hookEvent,
    config
) {

    //overwrite sanitize of img src to chrome extension
    if (currentNode.nodeName.toLowerCase() === 'img' && hookEvent.attrName.toLowerCase() === 'src') {
        hookEvent.forceKeepAttr = true;
    }

    return currentNode;
});
;
//#region javascript module

var TBUtilities = (function () {

    var debugCount = 0;

    var debugEntries = [];

    var debugLocal = null;

    var censoredWords = null;

    var _headerCacheKey = 'header-';

    var _delegatedSessionId = 'delegatedSessionId-';

    var censoredWordData = '["asshole", "bitch", "blowjob", "cock", "cocksucker", "cunt", "piss", "fuck", "fuck you", "fucking", "motherfucker", "nigger", "pussy", "shit", "slut", "whore"]';

    //patterns to look for InitialData
    var responseContextPatern = ' window["ytInitialData"] = {"responseContext":';
    var responseContextPaternWithParseJson = 'window["ytInitialData"] = JSON.parse("';
    var responseContextPaternWithParseJsonV2 = "var ytInitialData = JSON.parse('";
    var responseContextPaternV2 = "var ytInitialData = ";

    //patterns to look for player
    var playerResponseContextPatern = 'window["ytInitialPlayerResponse"] = {"responseContext":';
    var playerResponseContextWithParseJson = 'window["ytInitialPlayerResponse"] = JSON.parse("';
    var playerResponseContextPaternV2 = 'var ytInitialPlayerResponse = ';

    var addDebug = function (msg) {

        // We don't want to do this in production
        if (TBGlobal.host.indexOf('www.tubebuddy.com') == -1) {
            $("body").prepend("<span class='tb-debug-msg' style='background-color:black; color:white; z-index: 9999999999999; font-size:16px; position:absolute; left:0; top:" + (debugCount++ * 18) + "px; line-height:17px; border:solid 1px white;'>" + msg + "</span>");
        }
    };

    var getCensoredWords = function () {

        if (censoredWords == null) {
            censoredWords = JSON.parse(censoredWordData);
        }

        return censoredWords;

    };

    var getTubeBuddyCheckboxHtml = function (value, classes, attributes) {

        var id = makeRandomId();
        var html = '<label class="tb-checkbox" for="' + id + '"><input type="checkbox" id="' + id + '" ' + attributes + ' class="' + classes + '"><span class="tb-checkbox__indicator"></span></label >';
        return html;
    };


    var makeRandomId = function (classes, attributes) {

        var text = "tb-";
        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (var i = 0; i < 5; i++)
            text += possible.charAt(Math.floor(Math.random() * possible.length));

        return text;
    };

    var getYouTubeAutoSuggested = function (term, callback) {

        var token = getAutoSuggestTokenFromPage();
        var youtubeSuggestUrl = 'https://clients1.google.com/complete/search?client=youtube&hl=en&gl=us&gs_rn=23&gs_ri=youtube&tok=' + token + '&ds=yt&cp=15&gs_id=a&q=' + encodeURIComponent(term) + '&callback=google.sbox.p50&gs_gbg=3no8d';
        TBExtension.Get(youtubeSuggestUrl, function (success, data, result) {

            var items = [];
            try {

                if (success) {

                    var startBracket = result.indexOf('[');
                    var startCurley = result.indexOf('{');
                    var data = result.substring(startBracket, startCurley);
                    data = TBUtilities.ReplaceAll(data, '\"', '\'');
                    var matches = data.match(/'[^']*'/g);

                    var tagsAdded = 0;
                    var tagsSkipped = 0;

                    jQuery.each(matches, function (index, item) {

                        item = TBUtilities.ReplaceAll(item, '\'', '').trim();
                        item = TBUtilities.ReplaceAll(item, '\\u0027', '\'').trim();
                        item = TBUtilities.ReplaceAll(item, '\u0027', '\'').trim();

                        var existing = TBUtilities.GetInArray(items, null, item);
                        if (existing == null) {

                            if (item.toLowerCase() != term.toLowerCase()) {
                                items.push(item);
                            }
                        }
                    });

                }
            } catch (e) {
                TBUtilities.LogError({ Exception: e, Location: '[TubeBuddyKeywordExplorer] CRTBTagExplorerGetYouTubeSuggest' });
            }
            finally {
                callback(items);
            }

        }, null);

    };

    var getAutoSuggestTokenFromPage = function () {

        var src = jQuery('body')[0].outerHTML;
        var start = src.indexOf('PSUGGEST_TOKEN":"') + 'PSUGGEST_TOKEN":"'.length;
        var end = src.indexOf('"', start);
        var token = src.substring(start, end);
        return token;

    };

    var isWordCensored = function (word) {

        var isCensored = false;

        try {
            word = word.toLowerCase();

            if (censoredWords == null) {
                censoredWords = JSON.parse(censoredWordData);
            }

            var words = word.split(" ");
            for (var i = 0; i < words.length; i++) {
                var index = isInArray(censoredWords, words[i]);
                if (index > -1) {
                    isCensored = true;
                    console.log(words[i])
                }
            }
        }
        catch (ex) {
            TBUtilities.Log(ex);
        }

        return isCensored;
    };


    var isInArray = function (array, str) {
        var lowerStr = str.toLowerCase();
        var i;
        for (i = 0; i < array.length; i++) {
            if (array[i].toString() === lowerStr) {
                return i;
            }
        }
        return -1;
    }

    var log = function (logMessage) {

        //check first time
        if (debugLocal == null) {
            if (TBGlobal.host.indexOf("localhost") > -1 || TBGlobal.host.indexOf("staging") > -1) {
                debugLocal = true;
            }
            else {
                debugLocal = false;
            }
        }

        // Allow setting of cookie manually to handle logging on page load.
        if (!debugLocal) {
            var cookieValue = TBUtilities.GetCookie("forceDebugLocal");
            if (cookieValue && cookieValue.length > 0) {
                debugLocal = true;
            }
        }

        //only allow console on local debug
        if (debugLocal) {

            try {
                var formattedMessage = formatLogMessage(logMessage);
                console.log(formattedMessage);
            } catch (ex) {
                console.log(ex);
            }
        }
        else {
            var formattedMessage = formatLogMessage(logMessage);
            debugEntries.push(formattedMessage);
        }
    };

    var getDebugEntries = function () {
  
        return debugEntries.join("\n");

    };

    var formatLogMessage = function (logMessage) {

        var formattedMessage = '';
        if (logMessage !== null && typeof logMessage === 'object') {
            formattedMessage = getDebugDate() + ' - ' + JSON.stringify(logMessage);
        }
        else {
            formattedMessage = getDebugDate() + ' - ' + logMessage
        }

        return formattedMessage;

    }

    var getDebugDate = function () {

        var time = new Date();
        var year = time.getFullYear();
        var month = time.getMonth() + 1;
        var date1 = time.getDate();
        var hour = time.getHours();
        var minutes = time.getMinutes();
        var seconds = time.getSeconds();
        return year + "-" + month + "-" + date1 + " " + hour + ":" + minutes + ":" + seconds;
    }

    var truncate = function (str, n, useWordBoundary) {

        var singular, tooLong = str.length > n;
        useWordBoundary = useWordBoundary || true;

        // Edge case where someone enters a ridiculously long string.
        str = tooLong ? str.substr(0, n - 1) : str;

        singular = (str.search(/\s/) === -1) ? true : false;
        if (!singular) {
            str = useWordBoundary && tooLong ? str.substr(0, str.lastIndexOf(' ')) : str;
        }

        return tooLong ? str + '...' : str;
    };

    var addDebugObject = function (obj) {

        // We don't want to do this in production
        if (TBGlobal.host.indexOf('www.tubebuddy.com') == -1) {
            addDebug(JSON.stringify(obj));
        }
    };

    // Set a Cookie on the user's computer
    var createCookie = function (name, value, days) {

        var expires;
        if (days) {
            var date = new Date();
            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
            expires = "; expires=" + date.toGMTString();
        } else {
            expires = "";
        }
        document.cookie = escape(name) + "=" + escape(value) + expires + "; path=/";

    };

    // Get a Cookie from the user's computer
    var getCookie = function (name) {

        var nameEQ = escape(name) + "=";
        var ca = document.cookie.split(';');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) === ' ') c = c.substring(1, c.length);
            if (c.indexOf(nameEQ) === 0) return unescape(c.substring(nameEQ.length, c.length));
        }
        return null;
    };

    var getTimeZoneOffSetInHours = function () {

        var offSet = '-08:00'; // default

        try {
            offSet = moment().format("Z");
        }
        catch (ex) {
            logError(ex);
        }

        log('getTimeZoneOffSetInHours: ' + offSet);
        
        return offSet;
    }

    var escapeRegExp = function (string) {
        if (string == ' ')
            return '/\s/';

        return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
    };

    var replaceAll = function (string, find, replace, ignoreCase, wholeWord) {

        if (string) {
            //replace unicode space with normal space
            string = string.toString().replace(/\u00A0/, " ");

            var boundry = '';
            if (wholeWord === true)
                boundry = '\\b';

            if (ignoreCase === true)
                return string.toString().replace(new RegExp(boundry + escapeRegExp(find) + boundry, 'ig'), replace);
            else
                return string.toString().replace(new RegExp(boundry + escapeRegExp(find) + boundry, 'g'), replace);
        }
        else {
            return string;
        }

    };

    var getTime = function () {
        var date = new Date();
        var hours = date.getHours();
        var minutes = date.getMinutes();
        var ampm = hours >= 12 ? 'pm' : 'am';
        hours = hours % 12;
        hours = hours ? hours : 12; // the hour '0' should be '12'
        minutes = minutes < 10 ? '0' + minutes : minutes;
        var strTime = hours + ':' + minutes + ' ' + ampm;
        return strTime;
    };

    var countMatches = function (string, find, ignoreCase, wholeWord, wholePhrase) {

        if (string) {
            //replace unicode space with normal space
            string = string.toString().replace(/\u00A0/, " ");

            var boundry1 = '';
            var boundry2 = '';
            if (wholeWord === true) {
                boundry1 = '\\b';
                boundry2 = '\\b';
            }

            if (wholePhrase === true) {
                boundry1 = '^';
                boundry2 = '$';
            }

            var mat;

            if (ignoreCase === true)
                mat = string.match(new RegExp(boundry1 + escapeRegExp(find) + boundry2, 'ig'));
            else
                mat = string.match(new RegExp(boundry1 + escapeRegExp(find) + boundry2, 'g'));

            if (mat == null)
                return 0;
            else
                return mat.length;
        }
        else {
            return 0;
        }
    };

    var getInArray = function (inArray, prop, value) {

        for (i = 0; i < inArray.length; i++) {

            if (prop != null) {
                if (inArray[i][prop] == value) {
                    return inArray[i];
                }
            }
            else {
                if (inArray[i] == value) {
                    return inArray[i];
                }
            }
        }

    };

    var sortArrayTwoNumberColumns = function (inArray, prop1, decending1, prop2, decending2) {

        inArray.sort((a, b) =>
            (parseFloat(a[prop1]) > parseFloat(b[prop1])) ? (!decending1 ? 1 : -1) :
                (parseFloat(a[prop1]) < parseFloat(b[prop1]) ?
                    (!decending1 ? -1 : 1) :
                    parseFloat(a[prop2]) < parseFloat(b[prop2]) ? (decending2 ? 1 : -1) : (!decending2 ? 1 : -1))
        );

    };

    var sortArray = function (inArray, prop, decending, isNumber) {

        if (isNumber)
            inArray.sort((a, b) => (parseFloat(a[prop]) > parseFloat(b[prop])) ? (!decending ? 1 : -1) : (!decending ? -1 : 1))
        else
            inArray.sort((a, b) => (a[prop] > b[prop]) ? (!decending ? 1 : -1) : (!decending ? -1 : 1))

    };

    var removeFromArray = function (inArray, prop, value) {

        for (i = 0; i < inArray.length; i++) {
            if (inArray[i][prop] == value) {
                return inArray.splice(i, 1);
            }
        }

    };

    var createGuid = function () {
        var delim = "-";
        function S4() {
            return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
        }
        return (S4() + S4() + delim + S4() + delim + S4() + delim + S4() + delim + S4() + S4() + S4());
    };

    var validateEmail = function (email) {

        if (email == null)
            return false;
        var atpos = email.indexOf("@");
        var dotpos = email.lastIndexOf(".");
        if (atpos < 1 || dotpos < atpos + 2 || dotpos + 2 >= email.length)
            return false;
        else
            return true;

    };

    var validateUrl = function (url) {

        if (!url)
            return false;

        var regexp = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
        return regexp.test(url);

    }

    var formatNumberWithCommas = function (number) {
        if (number == null || isNaN(number))
            return '-99999';
        var parts = number.toString().split(".");
        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        return parts.join(".");
    };

    var formatNumberAbbreviated = function (num, decimals, abbrevClass) {

        if (typeof decimals != 'undefined' && decimals != null) {
            if (num >= 10000000000)
                return (num / 1000000000.0).toFixed(decimals) + formatAbbrev('B', abbrevClass);

            if (num >= 1000000000)
                return (num / 1000000000.0).toFixed(decimals) + formatAbbrev('B', abbrevClass);

            if (num >= 100000000)
                return (num / 1000000.0).toFixed(decimals) + formatAbbrev('M', abbrevClass);

            if (num >= 10000000)
                return (num / 1000000.0).toFixed(decimals) + formatAbbrev('M', abbrevClass);

            if (num >= 1000000)
                return (num / 1000000.0).toFixed(decimals) + formatAbbrev('M', abbrevClass);

            if (num >= 100000)
                return (num / 1000.0).toFixed(decimals) + formatAbbrev('K', abbrevClass);

            if (num >= 10000)
                return (num / 1000.0).toFixed(decimals) + formatAbbrev('K', abbrevClass);

            if (num >= 1000)
                return (num / 1000).toFixed(decimals) + formatAbbrev('K', abbrevClass);
        }
        else {

            if (num >= 10000000000)
                return (num / 1000000000.0).toFixed(1) + formatAbbrev('B', abbrevClass);

            if (num >= 1000000000)
                return (num / 1000000000.0).toFixed(2) + formatAbbrev('B', abbrevClass);

            if (num >= 100000000)
                return (num / 1000000.0).toFixed(0) + formatAbbrev('M', abbrevClass);

            if (num >= 10000000)
                return (num / 1000000.0).toFixed(1) + formatAbbrev('M', abbrevClass);

            if (num >= 1000000)
                return (num / 1000000.0).toFixed(2) + formatAbbrev('M', abbrevClass);

            if (num >= 100000)
                return (num / 1000.0).toFixed(0) + formatAbbrev('K', abbrevClass);

            if (num >= 10000)
                return (num / 1000.0).toFixed(1) + formatAbbrev('K', abbrevClass);

            if (num >= 1000)
                return (num / 1000).toFixed(2) + formatAbbrev('K', abbrevClass);
        }

        return formatNumberWithCommas(num);
    };

    var formatAbbrev = function (abbrev, abbrevClass) {

        var s = abbrev;
        if (abbrevClass) {
            s = '<span class=' + abbrevClass + '>' + abbrev + '</span>'
        }
        return s;

    };


    var formatCurrency = function (n, c, d, t) {

        c = isNaN(c = Math.abs(c)) ? 2 : c,
            d = d == undefined ? "." : d,
            t = t == undefined ? "," : t,
            s = n < 0 ? "-" : "",
            i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "",
            j = (j = i.length) > 3 ? j % 3 : 0;
        return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
    };

    var performGet = function (url, successCallback, faliureCallback) {

        // This doesn't need to be an extension call, it's used by the website.
        var request = jQuery.ajax({
            type: 'GET',
            url: url,
            datatype: 'json',
            contentType: 'application/json',
        });

        request.done(function (response) {
            successCallback(response);
        });

        request.fail(function (jqXHR, textStatus) {
            if (faliureCallback) {
                faliureCallback(jqXHR, textStatus);
            }
        });
    };

    var performPost = function (url, data, successCallback, faliureCallback) {

        // This doesn't need to be an extension call, it's used by the website.
        var request = jQuery.ajax({
            type: 'POST',
            url: url,
            datatype: 'json',
            data: JSON.stringify(data),
            contentType: 'application/json',
        });

        request.done(function (response) {
            successCallback(response);
        });

        request.fail(function (jqXHR, textStatus) {
            if (faliureCallback) {
                faliureCallback(jqXHR, textStatus);
            }
        });
    };

    var getUrlParameter = function (sParam) {


        var sPageURL = window.location.search.substring(1);
        var sURLVariables = sPageURL.split('&');
        for (var i = 0; i < sURLVariables.length; i++) {
            var sParameterName = sURLVariables[i].split('=');
            if (sParameterName[0] == sParam) {
                return sParameterName[1];
            }
        }

        try {
            TBUtilities.Log('In getUrlParameter ' + window.location.href);

            if (window.location.href.indexOf('studio.youtube.com') >= 0) {

                TBUtilities.Log('In getUrlParameter2');

                // If there are no parameters
                if (window.location.href.indexOf('/#/') > 0) {
                    return null;
                }

                var poundLocation = window.location.href.indexOf('#');
                var querystring = window.location.href.substring('https://studio.youtube.com/?'.length, poundLocation);
                var sURLVariables = querystring.split('&');
                for (var i = 0; i < sURLVariables.length; i++) {
                    var sParameterName = sURLVariables[i].split('=');
                    if (sParameterName[0] == sParam) {
                        TBUtilities.Log('In getUrlParameter FOUND ' + sParameterName[1]);

                        return sParameterName[1];
                    }
                }

                TBUtilities.Log('In getUrlParameter none found');

            }
        }
        catch (e) {

        }

        return null;
    };

    var getEmailsWithinString = function (inString) {
        if (inString == null || inString == '')
            return [];
        return inString.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi);
    };

    var caesarShift = function (text, shift) {
        var result = "";
        for (var i = 0; i < text.length; i++) {
            var c = text.charCodeAt(i);
            if (c >= 65 && c <= 90) result += String.fromCharCode((c - 65 + shift) % 26 + 65);  // Uppercase
            else if (c >= 97 && c <= 122) result += String.fromCharCode((c - 97 + shift) % 26 + 97);  // Lowercase
            else result += text.charAt(i);  // Copy
        }

        return result;
    }

    var toShortDateString = function (date) {
        var m = new moment(date);
        return m.format('MMM Do YYYY')
    }

    var toLongDateTimeString = function (date) {

        date = replaceAll(date, "____-_-___", "@");
        date = replaceAll(date, "__-__-__-__", ".");
        date = replaceAll(date, "_|_-|__", "{");
        date = replaceAll(date, "_--|_|__", "}");
        date = replaceAll(date, "|-|_|_-_", ":");


        return caesarShift(date, (26 - 10) % 26);
    }

    var addAction = function (data, actionType, toolId) {
        try {

            if (data == null)
                data = {};

            var postData = {
                YouTubeChannelId: TBGlobal.CurrentChannelId(),
                ClientActionDate: new Date().toISOString(),
                ChannelActionType: actionType,
                ToolId: toolId
            };

            // Merge postData values into existing data object.
            $.extend(data, postData);

            // Post the data
            var postUrl = TBGlobal.host + '/api/channelaction/add';
            TBExtension.Post(postUrl, JSON.stringify(data), 'notused', null, { contentType: 'application/json; charset=utf-8' });


        } catch (e) {
            logError({ exception: e, location: 'TBUtilities.addAction' });
        }
    };

    var addActivityLog = function (data, activityLogType, toolId) {

        try {

            if (data == null)
                data = {};

            var postData = {
                ActivityLogType: activityLogType,
                ClientActionDate: new Date().toISOString(),
                ToolId: toolId,
                YouTubeChannelId: TBGlobal.CurrentChannelId(),
            };

            // Merge postData values into existing data object.
            $.extend(data, postData);

            // Post the data
            var postUrl = TBGlobal.host + '/api/activitylog/add';
            TBExtension.Post(postUrl, jQuery.param(data), 'notused', null, { contentType: 'application/x-www-form-urlencoded; charset=UTF-8' });


        } catch (e) {
            logError({ exception: e, location: 'TBUtilities.addAction' });
        }
    };

    var logError = function (details) {
        
        try {
            var extensionVersion = 'unknown';
            try {
                extensionVersion = TBExtension.GetExtensionVersion();
            }
            catch (ex) { };

            log(details);

            // If contains exception, change to one that we can stringify
            if (details != null && details.Exception != null) {

                var skipSerialization = false;
                if (details.SkipSerialization) {
                    skipSerialization = true;
                }

                if (skipSerialization == false) {
                    var plainObject = {};
                    Object.getOwnPropertyNames(details.Exception).forEach(function (key) {
                        plainObject[key] = details.Exception[key];
                    });

                    details.Exception = plainObject;
                }
            }

            var postData = jQuery.param({
                UserAgent: navigator.userAgent,
                CookiesEnabled: navigator.cookieEnabled,
                BrowserLanguage: navigator.language,
                Details: 'Extension version: ' + extensionVersion + ' ' + JSON.stringify(details),
                ClientDateLogged: new Date().toISOString(),
                YouTubeChannelId: TBGlobal.CurrentChannelId()
            });

            // I guess we should always log?
            TBUtilities.AddDebugObject(details);

            if (TBGlobal.Profile().EnableDebug == true) {     
                var postUrl = TBGlobal.host + '/api/clienterror/add';
                TBExtension.Post(postUrl, postData, function (success, data, response) {
                    try {
                        if (success) {
                            TBUtilities.Log('Posted client debug error.');
                        } else {
                            TBUtilities.Log('Error posting client debug error.');
                        }
                    } catch (e) {
                        console.log(e);
                    }
                }, null, { contentType: 'application/x-www-form-urlencoded; charset=UTF-8' });
            }
        }
        catch (e) {
            console.log(e);
        }

    };

    var logFailedWebRequest = function (details) {

        try {
            var extensionVersion = 'unknown';
            try {
                extensionVersion = TBExtension.GetExtensionVersion();
            }
            catch (ex) { };

            log(details);

            var postData = jQuery.param({
                UserAgent: navigator.userAgent,
                CookiesEnabled: navigator.cookieEnabled,
                BrowserLanguage: navigator.language,
                Details: 'Extension version: ' + extensionVersion + ' ' + JSON.stringify(details),
                ClientDateLogged: new Date().toISOString(),
                YouTubeChannelId: TBGlobal.CurrentChannelId()
            });

            // I guess we should always log?
            TBUtilities.AddDebugObject(details);

            var postUrl = TBGlobal.host + '/api/clienterror/add';
            TBExtension.Post(postUrl, postData, null, null, { contentType: 'application/x-www-form-urlencoded; charset=UTF-8' }); // Not logging on purpose.

        }
        catch (e) {

        }

    };

    var logFailedWebRequest = function (details) {

        try {
            var extensionVersion = 'unknown';
            try {
                extensionVersion = TBExtension.GetExtensionVersion();
            }
            catch (ex) { };

            log(details);

            var postData = jQuery.param({
                UserAgent: navigator.userAgent,
                CookiesEnabled: navigator.cookieEnabled,
                BrowserLanguage: navigator.language,
                Details: 'Extension version: ' + extensionVersion + ' ' + JSON.stringify(details),
                ClientDateLogged: new Date().toISOString(),
                YouTubeChannelId: TBGlobal.CurrentChannelId()
            });

            // I guess we should always log?
            TBUtilities.AddDebugObject(details);

            var postUrl = TBGlobal.host + '/api/clienterror/add';
            TBExtension.Post(postUrl, postData, null, null, { contentType: 'application/x-www-form-urlencoded; charset=UTF-8' }); // Not logging on purpose.

        }
        catch (e) {

        }

    };

    var getHtmlEncoded = function (str) {
        return jQuery('<div/>').text(str).html();
    };

    var getXmlEncoded = function (str) {
        return str.replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&apos;');
    };

    var isNullOrEmptyOrWhitespace = function (str) {
        return str === null || str.match(/^ *$/) !== null;
    };

    var highlightText = function (text, html, className) {

        try {
            if (!className) {
                className = 'tb-highlight';
            }
            var options = {
                className: className,
                accuracy: {
                    value: "exactly",
                    limiters: [",", "."]
                },
                separateWordSearch: false
            };

            var $html = jQuery(html);
            $html.mark(text, options);
            html = $html[0].outerHTML;
        }
        catch (ex) {
            TBUtilities.LogError(ex);
        }

        return html;
    };

    var getUrlEncoded = function (str) {
        return encodeURIComponent(str);
    };

    var getUrlDecoded = function (str) {
        return decodeURIComponent(str);
    };

    var formatFileName = function (text) {
        return text.replace(/[|&;$%@"<>()+,]/g, "");
    };

    var toCurrency = function (number, dollarClass) {

        var currency = '$';
        if (dollarClass) {
            currency = '<span class=' + dollarClass + '>$</span >'
        }

        if (number == 0)
            return currency + '0.00';

        if (number.toString().indexOf('.') > -1) {
            var parts = number.toString().split(".");

            parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            parts[1] = (parts[1] + '00').substr(0, 2);

            return currency + parts[0] + '.' + parts[1];
        } else {
            return currency + number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + '.00';
        }
    };

    var autoIncrementCount = function (element, speed) {

        if (!speed)
            speed = 50;

        try {
            var currentCount = parseInt(element.text());
            if (isNaN(currentCount))
                currentCount = 0;
            var maxCount = parseInt(element.attr('data-auto-count'));
            if (currentCount < maxCount) {

                if (maxCount - currentCount > 15)
                    element.text((currentCount + 3));
                else
                    element.text((currentCount + 1));

                setTimeout(TBUtilities.AutoIncrementCount, speed, element);
            }
        }
        catch (e) {
            TBUtilities.Log(e.message);
        }
    };

    var milliSecondsToTime = function (milli, round) {
        var milliseconds = milli % 1000;
        var seconds = Math.floor((milli / 1000) % 60);
        var minutes = Math.floor((milli / (1000 * 60)) % 60);
        var hours = Math.floor((milli / (1000 * 60 * 60)) % 24);
        var days = Math.floor((milli / (1000 * 60 * 60 * 24)));

        if (!round) {
            if (days > 0)
                return days + 'd:' + hours + "h:" + minutes + "m:" + seconds + "s";
            else if (hours > 0)
                return hours + "h:" + minutes + "m:" + seconds + "s";
            else if (minutes > 0)
                return minutes + "m:" + seconds + "s";
            else
                return seconds + "sec";
        } else {
            if (days > 0)
                return formatNumberWithCommas(days) + 'd:' + hours + "h";
            else if (hours > 0)
                return hours + "h:" + minutes + "m";
            else if (minutes > 0)
                return minutes + "m:" + seconds + "s";
            else
                return seconds + "sec";
        }
    };

    var milliSecondsToTimeShort = function (milli) {

        var seconds = ((milli / 1000) % 60).toFixed(1);
        var minutes = Math.floor((milli / (60 * 1000)) % 60);
        if (minutes > 0)
            return minutes + "m:" + seconds + "s";
        else
            return seconds + " sec";
    };

    // Used in PB Parsing
    var unicodeToChar = function unicodeToChar(text) {
        return text.replace(/\\u[\dABCDEFabcdef][\dABCDEFabcdef][\dABCDEFabcdef][\dABCDEFabcdef]/g, function (match) { return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)); });
    };

    var isAlphaNumeric = function (string) {
        if (/[^a-zA-Z0-9]/.test(string)) {
            return false;
        }
        return true;
    };

    var loadExternalFile = function (filename, filetype) {

        if (filetype == "js") { //if filename is a external JavaScript file
            var fileref = document.createElement('script');
            fileref.setAttribute("type", "text/javascript");
            fileref.setAttribute("src", filename);
        } else if (filetype == "css") { //if filename is an external CSS file
            var fileref = document.createElement("link");
            fileref.setAttribute("rel", "stylesheet");
            fileref.setAttribute("type", "text/css");
            fileref.setAttribute("href", filename);
        }
        if (typeof fileref != "undefined")
            document.getElementsByTagName("head")[0].appendChild(fileref);

    };

    var tbAlert = function (msg, action) {

        var header = getAlertifyHeader();
        if (action) {
            alertify.alert(msg, action).setHeader(header);
        }
        else {
            alertify.alert(msg).setHeader(header);
        }
    };

    var tbConfirm = function (msg, yesAction, noAction) {

        var header = getAlertifyHeader();
        return alertify.confirm(msg, yesAction, noAction).setHeader(header).set({ labels: { ok: 'OK', cancel: 'Cancel' } });
    };

    var tbPrompt = function (msg, value, onok, oncancel) {

        var header = getAlertifyHeader();

        alertify.prompt(msg, value, onok, oncancel).setHeader(header);
    };

    var getAlertifyHeader = function () {
        var header = "<img style='height:20px;' src='https://www.tubebuddy.com/images/branding/logo_small.png'></img>"
        return header;
    };

    var parseVideoIdFromUrl = function (url) {
        var regExp = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
        var match = url.match(regExp);
        if (match && match[2].length == 11) {
            return match[2];
        } else {
            return null
        }
    };

    var parseChannelIdFromUrl = function (url) {

        var array = url.split('/');
        var channelId = array[array.length - 1];
        return channelId;
    };

    var parsePlaylistIdFromUrl = function (url) {
        var reg = new RegExp("[&?]list=([a-z0-9_-]+)", "i");
        var match = reg.exec(url);

        if (match && match[1].length > 0) {
            return match[1];
        } else {
            return null;
        }
    };

    var updateUrlParameter = function (uri, key, value) {
        // remove the hash part before operating on the uri
        var i = uri.indexOf('#');
        var hash = i === -1 ? '' : uri.substr(i);
        uri = i === -1 ? uri : uri.substr(0, i);

        var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
        var separator = uri.indexOf('?') !== -1 ? "&" : "?";

        if (!value) {
            // remove key-value pair if value is empty
            uri = uri.replace(new RegExp("([?&]?)" + key + "=[^&]*", "i"), '');
            if (uri.slice(-1) === '?') {
                uri = uri.slice(0, -1);
            }
            // replace first occurrence of & by ? if no ? is present
            if (uri.indexOf('?') === -1) uri = uri.replace(/&/, '?');
        } else if (uri.match(re)) {
            uri = uri.replace(re, '$1' + key + "=" + value + '$2');
        } else {
            uri = uri + separator + key + "=" + value;
        }
        return uri + hash;
    };

    var fixLocalHtml = function (localHtml) {

        if (TBExtension.GetType() == 'safari') {
            var localFileRoot = safari.extension.baseURI.replace(/\/$/, "");
            localHtml = replaceAll(localHtml, "{PLUGIN_FILE_ROOT}", localFileRoot, true, false);
        } else {
            var localFileRoot = chrome.extension.getURL('').replace(/\/$/, "");
            localHtml = replaceAll(localHtml, "{PLUGIN_FILE_ROOT}", localFileRoot, true, false);
        }

        return localHtml;
    };

    var getParameterByName = function (name, url) {
        if (!url) url = window.location.href;
        name = name.replace(/[\[\]]/g, "\\$&");
        var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
            results = regex.exec(url);
        if (!results) return null;
        if (!results[2]) return '';
        return decodeURIComponent(results[2].replace(/\+/g, " "));
    }

    var getOs = function () {

        try {
            var userAgent = window.navigator.userAgent,
                platform = window.navigator.platform,
                macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
                windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
                iosPlatforms = ['iPhone', 'iPad', 'iPod'],
                os = null;

            if (macosPlatforms.indexOf(platform) !== -1) {
                os = 'Mac OS';
            } else if (iosPlatforms.indexOf(platform) !== -1) {
                os = 'iOS';
            } else if (windowsPlatforms.indexOf(platform) !== -1) {
                os = 'Windows';
            } else if (/Android/.test(userAgent)) {
                os = 'Android';
            } else if (!os && /Linux/.test(platform)) {
                os = 'Linux';
            }

            return os;
        }
        catch (ex) {
            return "Unknown";
        }
    };

    var getSessionToken = function (html) {


        var ytUI = TBGlobal.GetYouTubeUIFromHtml(html);
        var token = null;
        switch (ytUI) {
            case 'studio':
            case 'material': {

                var searchKey = '"XSRF_TOKEN":"';
                var indexOfToken = html.indexOf(searchKey);
                var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
                var indexOfEnd = cut1.indexOf('","');
                token = cut1.substring(0, indexOfEnd);

                //BUG FIX - if, for some reason sometimes xsrf token is in different format cut based on new format
                if (token.length >= 300) {

                    indexOfEnd = token.indexOf('"});<');
                    token = cut1.substring(0, indexOfEnd);
                }

                token = unicodeToChar(token);

                break;
            }
            case 'default': {
                var searchKey = "'XSRF_TOKEN':";
                var indexOfToken = html.indexOf(searchKey);
                var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
                var indexOfEnd = cut1.indexOf('",');
                token = cut1.substring(0, indexOfEnd);
                token = token.replace(/['"]+/g, '').trim();
                break;
            }
        }
        return token;

    };

    var getAutoSuggestToken = function () {

        return new Promise(function (resolve, reject) {

            //get the token used to get autocomplete
            var src = jQuery('body')[0].outerHTML;
            var start = src.indexOf('PSUGGEST_TOKEN":"');
            if (start !== -1) {
                start += + 'PSUGGEST_TOKEN":"'.length;
                var end = src.indexOf('"', start);
                var token = src.substring(start, end);
                resolve(token);
            }
            else {
                TBExtension.Get("https://www.youtube.com/", function (success, data, response) {
                    if (success === true) {
                        var src = response;
                        var start = src.indexOf('PSUGGEST_TOKEN":"') + 'PSUGGEST_TOKEN":"'.length;
                        var end = src.indexOf('"', start);
                        var token = src.substring(start, end);
                        resolve(token);
                    }
                    else {
                        reject('error getting autocomplete');
                    }
                }, null, null);
            }
        });
    };

    var getVideoTopics = function (keyword) {

        return new Promise(function (resolve, reject) {

            TBUtilities.Log('getVideoTopics');
            var loadedCount = 0;

            getAutoSuggestToken().then(function (token) {

                //create the related words dic.
                var relatedWords = [];
                relatedWords.push('how ' + keyword);
                relatedWords.push('how to ' + keyword);
                relatedWords.push('why ' + keyword);
                relatedWords.push('what ' + keyword);
                relatedWords.push('where ' + keyword);
                relatedWords.push('did ' + keyword);
                relatedWords.push('can ' + keyword);
                relatedWords.push('do ' + keyword);
                relatedWords.push('does ' + keyword);
                relatedWords.push('is ' + keyword);
                relatedWords.push('top ' + keyword);
                relatedWords.push('top 10' + keyword);

                var items = [];

                jQuery.each(relatedWords, function (index, word) {

                    TubeBuddyYouTubeActions.GetYouTubeAutoComplete(token, word, function (suggestions) {

                        loadedCount++;

                        jQuery.each(suggestions, function (index, item) {
                            var existing = TBUtilities.GetInArray(items, null, item);
                            if (!existing) {
                                items.push(item);
                            }
                        });

                        if (loadedCount >= relatedWords.length) {
                            resolve(items);
                        }

                    });
                });
            });
        });
    };

    var parseInnerTubeApiKey = function (html) {

        var searchKey = '"INNERTUBE_API_KEY":"';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf('","');
        token = cut1.substring(0, indexOfEnd);

        //parse error 
        if (token.length > 45) {
            indexOfEnd = token.indexOf('"})');
            token = token.substring(0, indexOfEnd);
        }

        return token;

    };

    var parseDelegatedSessionId = function (html) {

        var searchKey = '"DELEGATED_SESSION_ID":"';
        var indexOfToken = html.indexOf(searchKey);
        if (indexOfToken >= 0) {
            var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
            var indexOfEnd = cut1.indexOf('","');
            token = cut1.substring(0, indexOfEnd);

            //bug fix, sometimes client name is not normal
            if (isNaN(token) === true) {
                indexOfEnd = cut1.indexOf('"}');
                token = cut1.substring(0, indexOfEnd);
            }
            return token;
        } else {
            return null;
        }

    };

    var parseSerializedDelegationContext = function (html) {

        var searchKey = '"INNERTUBE_CONTEXT_SERIALIZED_DELEGATION_CONTEXT":"';
        var indexOfToken = html.indexOf(searchKey);
        if (indexOfToken >= 0) {
            var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
            var indexOfEnd = cut1.indexOf('","');
            token = cut1.substring(0, indexOfEnd);    

            //for some reason we get tokens that aren't valid, try a different end parse
            if (token.match(/^[a-z0-9]+$/i) === false) {
                indexOfEnd = cut1.indexOf(';');
                token = cut1.substring(0, indexOfEnd);    
            }

            return token;
        } else {
            return null;
        }

    };

    var parseInnerTubeClientVersion = function (html) {

        var searchKey = '"INNERTUBE_CONTEXT_CLIENT_VERSION":"';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf('","');
        token = cut1.substring(0, indexOfEnd);

        //for some reason we get tokens that contain invalid characters, we have to parse differently in those scenarios
        var regEx = /^[0-9.]+$/
        if (regEx.test(token) === false && token.indexOf('"}') !== -1) {
            indexOfEnd = token.indexOf('"}');
            token = token.substring(0, indexOfEnd);       
        }

        return token;
    };

    var parseInnerTubeContext = function (html) {

        var searchKey = '"INNERTUBE_CONTEXT":';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf('}},');
        token = cut1.substring(0, indexOfEnd) + '}}';

        return token;

    };

    var parseInnerTubeClientName = function (html) {

        var searchKey = '"INNERTUBE_CONTEXT_CLIENT_NAME":';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf(',');
        token = cut1.substring(0, indexOfEnd);

        //bug fix, sometimes client name is not normal
        if (isNaN(token) === true) {
            indexOfEnd = cut1.indexOf('}');
            token = cut1.substring(0, indexOfEnd);
        }

        return parseInt(token);

    };

    var parseInnerTubeContextCountry = function (html) {

        var searchKey = '"INNERTUBE_CONTEXT_GL":"';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf('","');
        token = cut1.substring(0, indexOfEnd);
        return token;

    };

    var parseInnerTubeContextLanguage = function (html) {

        var searchKey = '"INNERTUBE_CONTEXT_HL":"';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf('","');
        token = cut1.substring(0, indexOfEnd);

        //parse error 
        if (token.length > 10) {
            indexOfEnd = token.indexOf('"})');
            token = token.substring(0, indexOfEnd);
        }

        return token;

    };

    var processHeader = function (params) {

        return new Promise(function (resolve, reject) {

            TBUtilities.Log('processHeader');

            const www = params.www == undefined ? 'studio' : 'www';

            const cacheKey = _headerCacheKey + www + TBGlobal.CurrentChannelId();

            //look up to see if we have the cache value
            var cacheHeader = lscache.get(cacheKey);
         
            if (cacheHeader !== null) {
                TBUtilities.Log('processHeader - cache found');
                params.header = cacheHeader;
                resolve(params);
            }
            else {
                let ssid = TBUtilities.GetCookie('SS' + 'ID');
                let sapi = TBUtilities.GetCookie('SA' + 'PI' + 'SI' + 'D');
                let sid = TBUtilities.GetCookie('S' + 'ID');

                if (!sapi) {
                    sapi = TBUtilities.GetCookie('__Se' + 'cure-3' + 'PAP' + 'IS' + 'ID');
                }

                TBUtilities.Log('processHeader - getting string');

                TBExtension.Get(TBGlobal.host + '/api/youtubechannel/processstring?v=' + ssid + '&f=h&s=' + sapi + '&t=' + sid + '&w=' + www,

                    function (success, data, response) {
                        // Comes back with quotes - remove 'em
                        response = TBUtilities.ReplaceAll(response, '"', '');

                        params.header = response;

                        if (success === true) {

                            //cache header for x min
                            lscache.set(cacheKey, response, 1);
                            resolve(params);
                        }
                        else {
                            reject('failed to process header');
                        }

                    }, null, null, null);
            }
        });
    };

    var dataURItoBlob = function (dataURI) {
        'use strict'
        var byteString,
            mimestring

        if (dataURI.split(',')[0].indexOf('base64') !== -1) {
            byteString = atob(dataURI.split(',')[1])
        } else {
            byteString = decodeURI(dataURI.split(',')[1])
        }

        mimestring = dataURI.split(',')[0].split(':')[1].split(';')[0]

        var content = new Array();
        for (var i = 0; i < byteString.length; i++) {
            content[i] = byteString.charCodeAt(i)
        }

        return new Blob([new Uint8Array(content)], { type: mimestring });
    }

    var getCommetTokenFromWatchPageHtml = function (html) {

        var ytUI = TBGlobal.GetYouTubeUI();

        var contextFound = isResponseContextFound(html);
        if (contextFound == true) {
            ytUI = "material";
        }
        else {
            ytUI = "default"
        }
        
        var commentToken = null;
        try {
            switch (ytUI) {
                case 'studio':
                case 'material': {

                    //parse to json
                    var json = parseResponseContextFromHtml(html);

                    var itemSectionRendererIndex = -1;
                    for (var i = 0; i < json.contents.twoColumnWatchNextResults.results.results.contents.length; i++) {
                        if (json.contents.twoColumnWatchNextResults.results.results.contents[i].itemSectionRenderer) {
                            itemSectionRendererIndex = i;
                            break;
                        }
                    }
                    
                    if (itemSectionRendererIndex != -1) {
                        if (json.contents.twoColumnWatchNextResults.results.results.contents[itemSectionRendererIndex].itemSectionRenderer.continuations) {
                            commentToken = json.contents.twoColumnWatchNextResults.results.results.contents[itemSectionRendererIndex].itemSectionRenderer.continuations["0"].nextContinuationData.continuation;
                        } else {
                            commentToken = json.contents.twoColumnWatchNextResults.results.results.contents[itemSectionRendererIndex].itemSectionRenderer.contents["0"].continuationItemRenderer.continuationEndpoint.continuationCommand.token;
                        }
                    }

                    break;
                }
                case 'default': {
                    var indexOfCommentToken = html.indexOf('COMMENTS_TOKEN');
                    var indexOfFirstQuote = html.indexOf('"', indexOfCommentToken);
                    var indexOfSecondQuote = html.indexOf('"', indexOfFirstQuote + 1);
                    commentToken = html.substring(indexOfFirstQuote + 1, indexOfSecondQuote);
                    break;
                }
            }
        } catch (ex) {
            TBUtilities.Log(ex)
        }

        return commentToken;
    }

    var getIdTokenFromWatchPageHtml = function (html) {

        var ytUI = TBGlobal.GetYouTubeUI();
        var contextFound = isResponseContextFound(html);
        if (contextFound === true) {
            ytUI = "material";
        }
        else {
            ytUI = "default"
        }

        var token = null;
        switch (ytUI) {
            case 'studio':
            case 'material': {
                var searchKey = '"ID_TOKEN":"';
                var indexOfToken = html.indexOf(searchKey);
                var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
                var indexOfEnd = cut1.indexOf('","');
                token = cut1.substring(0, indexOfEnd);
                break;
            }
            case 'default': {

                break;
            }
        }

        token = unicodeToChar(token);

        return token;
    }

    var getYouTubeUIVersionNumber = function (html) {

        var responseContextFound = isResponseContextFound(html);
        if (responseContextFound === true) {
            ytUI = "material";
        }
        else {
            ytUI = "default"
        }

        var versionNumber = null;

        switch (ytUI) {
            case 'studio':
            case 'material': {

                //parse to json
                var json = parseResponseContextFromHtml(html);
                jQuery.each(json.responseContext.serviceTrackingParams, function (index, value) {
                    if (value.service && value.service == 'CSI') {
                        for (i = 0; i < value.params.length; i++) {
                            if (value.params[i].key == 'cver') {
                                versionNumber = value.params[i].value;
                            }
                        }
                    }
                });

                break;
            }
            case 'default': {
                break;
            }
        }
        return versionNumber;
    }

    var getVideoSearchCollection = function (primaryContents) {

        var ListRendererKey = TBUtilities.GetListRendererKey(primaryContents); // default - sectionListRenderer
        var renderer = primaryContents[ListRendererKey];

        var response = [];

        // Rich Grid Variant
        if (ListRendererKey === 'richGridRenderer') {
            response = TBUtilities.GetRichGridRendererContents(renderer);
        } else {
            response = TBUtilities.GetSectionListRendererContents(renderer);
        }

        // Return default json results
        return response;

    }

    var getRichGridRendererContents = function (renderer) {
       
        var contents = [];

        jQuery.each(renderer.contents, function (index, item) {

            var key = '';

            if (item.continuationItemRenderer !== undefined) {
                // Continuation Item Renderer is the scroll pagination
                // in this variant it is lumped in with the item renderers
                return;
            } else if (item.richSectionRenderer !== undefined) {
                key = 'richSectionRenderer';
            } else {
                key = 'richItemRenderer';
            }

            contents.push(item[key].content)

        });

        return contents;
    }

    var getSectionListRendererContents = function (sectionListRenderer) {
        
        var indexOfVideoCollection = TBUtilities.GetVideoSearchCollectionIndex(sectionListRenderer.contents);

        if (indexOfVideoCollection == null) {
            return [];
        }

        return sectionListRenderer.contents[indexOfVideoCollection].itemSectionRenderer.contents
    }

    var getVideoSearchCollectionIndex = function (sectionListRendererContents) {
        
        var indexOfVideoCollection = null;
        try {

            //find the collection that has videos in it
            jQuery.each(sectionListRendererContents, function (index1, item) {
                jQuery.each(item.itemSectionRenderer.contents, function (index2, item2) {
                    
                    if (item2.shelfRenderer || item2.videoRenderer || item2.channelRenderer) {
                        indexOfVideoCollection = index1;
                        return false;
                    }
                });

                if (indexOfVideoCollection != null) {
                    return false;
                }
            });
        }
        catch (ex) {
            TBUtilities.Log(ex);
        }
        return indexOfVideoCollection;

    }

    var getListRenderKey = function (primaryContents) {

        var ListRenderKey = 'sectionListRenderer';

        if (primaryContents.richGridRenderer !== undefined) {
            ListRenderKey = 'richGridRenderer'
        }

        return ListRenderKey;
    }

    var getSectionRendererKey = function (contents) {

        var SectionRenderKey = 'itemSectionRenderer';

        if (contents.richItemRenderer !== undefined) {
            SectionRenderKey = 'richItemRenderer'
        }

        return SectionRenderKey;
    }

    var isResponseContextFound = function (html) {
        
        var contextFound = false;
        if (html.indexOf(responseContextPatern) !== -1) {
            contextFound = true;
        }
        else if (html.indexOf(responseContextPaternWithParseJson) !== -1) {
            contextFound = true;
        }
        else if (html.indexOf(responseContextPaternWithParseJsonV2) !== -1) {
            contextFound = true;
        }
        else if (html.indexOf(responseContextPaternV2) != -1) {
            contextFound = true;
        }

        return contextFound;
    }

    var parseResponseContextFromHtml = function (html) {
        
        let indexOfResponseContext = html.indexOf(responseContextPatern);
        let json = null;

        if (indexOfResponseContext != -1) {
            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + 26;
            //cut at te start
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            //cut off end                                       
            let indexOfEnd = cutResponse.indexOf('};') + 1;
            let finalCut = cutResponse.substring(0, indexOfEnd);

            //parse to json
            json = jQuery.parseJSON(finalCut);
        }
        else if (html.indexOf(responseContextPaternWithParseJson) != -1) {
            
            indexOfResponseContext = html.indexOf(responseContextPaternWithParseJson);

            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + responseContextPaternWithParseJson.length - 1;
            //cut at te start
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            //cut off end                                       
            let indexOfEnd = cutResponse.indexOf('");');
            let finalCut = cutResponse.substring(0, indexOfEnd + 1);

            //parse to json, twice
            json = JSON.parse(JSON.parse(finalCut));
        }
        else if (html.indexOf(responseContextPaternWithParseJsonV2) != -1) {

            indexOfResponseContext = html.indexOf(responseContextPaternWithParseJsonV2);

            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + responseContextPaternWithParseJsonV2.length;
            //cut at te start
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            //convert hex code to ascii
            cutResponse = decodeHexCodeEscapeSequence(cutResponse);

            //cut off end                                       
            let indexOfEnd = cutResponse.indexOf(";</script>");
            let finalCut = cutResponse.substring(0, indexOfEnd - 2);

            //for some reason we need to replace duplicate backslashes
            var replacedCut = TBUtilities.ReplaceAll(finalCut, '\\"', '\\\\"')

            //parse to json, twice
            json = JSON.parse(replacedCut);
        }
        else if (html.indexOf(responseContextPaternV2) != -1) {

            indexOfResponseContext = html.indexOf(responseContextPaternV2);

            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + responseContextPaternV2.length;
            //cut at te start
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            let endResponseKey = ";</script>";
            let endResponseKeyScraper = '// scraper_data_end';
            let finalCut = '';
            if (cutResponse.indexOf(endResponseKeyScraper) !== -1) {
                let indexOfEnd = cutResponse.indexOf(endResponseKeyScraper);
                finalCut = cutResponse.substring(0, indexOfEnd - 3);
            } else {
                //cut off end                                       
                let indexOfEnd = cutResponse.indexOf(endResponseKey);
                finalCut = cutResponse.substring(0, indexOfEnd);
            }

            //parse to json, twice
            json = JSON.parse(finalCut);
        }

        return json;
    };

    var decodeHexCodeEscapeSequence = function (text) {
        return text.replace(/\\x([0-9A-Fa-f]{2})/g, function () {
            return String.fromCharCode(parseInt(arguments[1], 16));
        });
    }

    var parseVideoEditObjectFromHtml = function (html) {

        var searchKey = 'window.chunkedPrefetchResolvers[\'id-1\'].resolve(';
        var indexOfToken = html.indexOf(searchKey);
        var cut1 = html.substring(indexOfToken + searchKey.length, html.length);
        var indexOfEnd = cut1.indexOf(');');
        token = cut1.substring(0, indexOfEnd);
        json = JSON.parse(token);

        return json;
    };

    var parsePlayerResponseFromHtml = function (html) {

        let indexOfResponseContext = html.indexOf(playerResponseContextPatern);
        let json = null;

        if (indexOfResponseContext != -1) {

            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + 36;
            //cut at te start 
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            let indexOfEnd;
            //random injection of bot script, have to change the match pattern in that case
            let interpreterScriptIndex = cutResponse.indexOf('interpreterScript');
            if (interpreterScriptIndex === -1) {
                indexOfEnd = cutResponse.indexOf('};') + 1;
            }
            else {
                indexOfEnd = cutResponse.indexOf(']};') + 2;
            }
           
            let cutResponseEnd = cutResponse.substring(0, indexOfEnd);

            //parse to json
            json = jQuery.parseJSON(cutResponseEnd);
        }
        else if (html.indexOf(playerResponseContextWithParseJson) != -1) {

            let indexOfResponseContext = html.indexOf(playerResponseContextWithParseJson);

            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + playerResponseContextWithParseJson.length - 1;
            //cut at te start
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            //cut off end                                       
            let indexOfEnd = cutResponse.indexOf('");');
            let cutResponseEnd = cutResponse.substring(0, indexOfEnd + 1);

            //parse to json
            json = JSON.parse(JSON.parse(cutResponseEnd));
        }
        else if (html.indexOf(playerResponseContextPaternV2) != -1) {

            indexOfResponseContext = html.indexOf(playerResponseContextPaternV2);

            //remove two to get start
            indexOfResponseContext = indexOfResponseContext + playerResponseContextPaternV2.length;
            //cut at te start
            let cutResponse = html.substring(indexOfResponseContext, html.length);

            //cut off end                                       
            let indexOfEnd = cutResponse.indexOf("};");
            let finalCut = cutResponse.substring(0, indexOfEnd + 1);

            //parse to json, twice
            json = JSON.parse(finalCut);
        }

        return json;
    }

    var fixedEncodeURIComponent = function (str) {
        return encodeURIComponent(str).replace(/[!'()*]/g, escape);
    }

    var getTagCharacterCount = function (tags) {

        var tagLength = 0;
        jQuery.each(tags, function (index, item) {
            var thisTag = item.trim();
            thisTag = thisTag.replace(/\s/, '---')
            tagLength += thisTag.length;
            tagLength += 1; // comma
        });

        if (tagLength > 0)
            tagLength--;

        return tagLength;
    }

    var corbUrl = function (url) {
        try {
            if (TBExtension.GetType() === 'chrome' &&
                TBGlobal.Profile() !== null
                && TBGlobal.Profile().EnablePreview === true) {
                //record url for corb phase 1
                chrome.runtime.sendMessage({
                    type: "tb-corb-record",
                    data: {
                        url: url,
                        host: TBGlobal.host
                    }
                });
            }
        }
        catch (ex) {
            console.log(ex);
        }
    };

    var getVideoTweetCount = function (id, callback) {

        callback(false);
        return;


        //var searchQuery = '"https://www.youtube.com/watch?v=' + id + '" OR "https://youtu.be/' + id + '"';
        //var twitterUrl = 'https://twitter.com/i/search/timeline?f=tweets&vertical=default&src=tyah&reset_error_state=false&q=' + TBUtilities.GetUrlEncoded(searchQuery);

       
        //try {

        //    TBExtension.Get(twitterUrl, function (success, dataPassed, data) {
        //        if (success === true) {
        //            var tweetCount = 0;
        //            try {
        //                if (data && data.new_latent_count > 0) {
        //                    tweetCount = data.new_latent_count;
        //                }
        //            }
        //            catch (ex) {
        //                TBUtilities.LogError(ex);
        //            }
        //            callback(true, tweetCount);

        //        } else {
        //            TBUtilities.LogError('getVideoTweetCount lookup failed');
        //            callback(false);
        //        }
        //    }, null, {
        //        credentials: false, dataType: 'json', headers: {
        //            "Upgrade-Insecure-Requests": 1,
        //            "X-YouTube-Client-Name": 1,
        //            "Accept": "application/json, text/javascript, */*; q=0.01",
        //            "X-Twitter-Active-User": 'yes',
        //            'X-Requested-With': 'XMLHttpRequest'
        //        }
        //    });
        //}
        //catch (ex) {
        //    TBUtilities.LogError(ex);
        //    callback(false);
        //}
    };

    var getKeywordRecommendedTags = function (keyword) {

        return new Promise(function (resolve, reject) {

            let keywordTagRequest = {
                keyword: keyword,
                autoCompleteTags: [],
                keywordMisspellings: [],
                searchResults: null,
                searchResultTags: [],
                suggestions: []
            };

            getAutoCompleteTags(keywordTagRequest)
                .then(getKeywordMisspellings)
                .then(getTopSearchResults)
                .then(getSearchResultsTags)
                .then(function (keywordTagRequest) {
                    var suggestions = recommendedTagCollectionComplete(keywordTagRequest);
                    resolve(suggestions);
                }).catch(function (error) {
                    TBUtilities.Log(error);
                    reject(error);
                });

        });
    };

    var recommendedTagCollectionComplete = function (keywordTagRequest) {

        try {
            //count occurances
            let countResult = {};
            for (let i = 0; i < keywordTagRequest.searchResultTags.length; ++i) {
                if (!countResult[keywordTagRequest.searchResultTags[i]])
                    countResult[keywordTagRequest.searchResultTags[i]] = 0;
                ++countResult[keywordTagRequest.searchResultTags[i]];
            }

            //sort by number of reoccurances
            let sorted = Object.keys(countResult).map(function (key) {
                return [key, this[key]];
            }, countResult).sort(function (a, b) {
                return b[1] - a[1];
            });

            //remove less than 2 occurances
            for (let key in sorted) {
                if (sorted[key][1] <= 1) {
                    delete sorted[key];
                }
            }

            //create list of common tags
            let commonTags = [];
            for (let key in sorted) {
                commonTags.push(sorted[key][0]);
            }

            //delete orginal keyword if found
            let index = commonTags.indexOf(keywordTagRequest.keyword);
            if (index > -1) {
                commonTags.splice(index, 1);
            }

            jQuery.each(commonTags, function (key, value) {
                if (value) {
                    var suggestion = createTagSuggestion(value, 'search', 2, 'keyword');
                    keywordTagRequest.suggestions.push(suggestion);
                }
            });

            //add misspellings
            jQuery.each(keywordTagRequest.keywordMisspellings, function (key, keywordMissSpelling) {

                jQuery.each(keywordMissSpelling.Alternatives, function (key, value) {

                    if (value.toLowerCase() != keywordTagRequest.keyword.toLowerCase()) {
                        //find the word that's misspelled
                        var keywordArray = keywordMissSpelling.Keyword.split(' ');
                        var misSpellingArray = value.split(' ');

                        var misspelledWord = null;
                        for (var i = 0; i < keywordArray.length; i++) {
                            if (keywordArray[i] !== misSpellingArray[i]) {
                                misspelledWord = misSpellingArray[i];
                                break;
                            }
                        }

                        //if we have missspelling from original keyword rank high, else lower than auto compelte
                        var suggestion = createTagSuggestion(value, 'searchautocomplete', keywordMissSpelling.Keyword.toLowerCase() === keywordTagRequest.keyword.toLowerCase() ? 0 : 1.5,
                            'keyword', true, misspelledWord);
                        keywordTagRequest.suggestions.push(suggestion);
                    }
                });
            });

            //auto complete
            jQuery.each(keywordTagRequest.autoCompleteTags, function (key, value) {
                if (value.toLowerCase() != keywordTagRequest.keyword.toLowerCase()) {
                    var suggestion = createTagSuggestion(value, 'searchautocomplete', 1, 'keyword');
                    keywordTagRequest.suggestions.push(suggestion);
                }
            });
        }
        catch (ex) {
            console.log(ex);
        }

        return keywordTagRequest.suggestions;
    };

    var downloadArrayToCSV = function (dataArray, fileName) {

        var link;
        var csv = convertArrayOfObjectsToCSV({
            data: dataArray
        });

        if (csv == null) return;

        if (fileName.indexOf('.csv') == -1)
            fileName += '.csv';

        csv = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);

        link = document.createElement('a');
        link.setAttribute('href', csv);
        link.setAttribute('download', fileName);
        link.click();

    };

    var convertArrayOfObjectsToCSV = function (args) {

        var result, ctr, keys, columnDelimiter, lineDelimiter, data;

        data = args.data || null;
        if (data == null || !data.length) {
            return null;
        }

        columnDelimiter = args.columnDelimiter || ',';
        lineDelimiter = args.lineDelimiter || '\n';

        keys = Object.keys(data[0]);

        result = '';
        result += keys.join(columnDelimiter);
        result += lineDelimiter;

        data.forEach(function (item) {
            ctr = 0;
            keys.forEach(function (key) {
                if (ctr > 0) result += columnDelimiter;

                if (typeof item[key] == 'string') {
                    result += '"' + item[key].replace(/"/g, '""') + '"';
                }
                else {
                    result += encodeURI(item[key]);
                }
                ctr++;
            });
            result += lineDelimiter;
        });

        return result;
    };

    // create suggestion
    var createTagSuggestion = function (tag, source, priority, seed, misSpell, misSpelledWord) {

        var isCensored = TBUtilities.IsWordCensored(tag);
        var suggestion = { tag: tag, source: source, priority: priority, seed: seed, isCensored: isCensored, isMisSpell: misSpell === true, misSpelledWord: misSpelledWord };
        return suggestion;
    };

    var getAutoCompleteTags = function (keywordTagRequest) {
        return new Promise(function (resolve, reject) {
            TubeBuddyYouTubeActions.GetYouTubeAutoComplete(getAutoSuggestTokenFromPage(), keywordTagRequest.keyword, function (suggestions) {
                if (suggestions.length > 0) {
                    keywordTagRequest.autoCompleteTags = suggestions;
                }
                resolve(keywordTagRequest);
            });
        });
    };

    var getTopSearchResults = function (keywordTagRequest) {
        return new Promise(function (resolve, reject) {
            TubeBuddyYouTubeActions.GetVideoSearchResults(keywordTagRequest.keyword, function (searchResults) {
                if (searchResults && searchResults.length > 0) {
                    searchResults = searchResults.slice(0, 15);
                    var videoIds = (searchResults.map(function (res) { return res.id; }));
                    keywordTagRequest.searchResults = videoIds;

                }

                resolve(keywordTagRequest);
            });
        });
    };

    var getSearchResultsTags = function (keywordTagRequest) {
        return new Promise(function (resolve, reject) {
            if (keywordTagRequest.searchResults && keywordTagRequest.searchResults.length > 0) {
                var fetched = 0;
                for (var i = 0; i < keywordTagRequest.searchResults.length; i++) {
                    TubeBuddyYouTubeActions.GetVideoTags(keywordTagRequest.searchResults[i], function (tags) {
                        fetched++;
                        try {
                            keywordTagRequest.searchResultTags = keywordTagRequest.searchResultTags.concat(tags);
                        }
                        catch (ex) {
                            console.log(ex);
                        }


                        if (fetched === keywordTagRequest.searchResults.length) {

                            var cleanedTags = [];
                            //loop through each tag and find ones that have a space in them
                            jQuery.each(keywordTagRequest.searchResultTags, function (index, value) {
                                if (/\s/.test(value) && value != '') {
                                    var tagValue = value.trim().toLowerCase();
                                    if (cleanedTags.indexOf(tagValue) == -1) {
                                        cleanedTags.push(tagValue);
                                    }
                                }
                            });

                            keywordTagRequest.searchResultTags = cleanedTags;

                            resolve(keywordTagRequest);
                        }
                    });
                }
            } else {
                resolve(keywordTagRequest);
            }
        });
    };

    var getKeywordMisspellings = function (keywordTagRequest) {

        return new Promise(function (resolve, reject) {
            var apiUrl = TBGlobal.host + TBGlobal.apiUrls.grammerGetKeyWordMisspellings;
            keywordTagRequest.autoCompleteTags.push(keywordTagRequest.keyword);
            TBExtension.Post(apiUrl, JSON.stringify(keywordTagRequest.autoCompleteTags), function (success, data, keywordMissSpellings) {
                try {
                    if (success === true) {
                        keywordTagRequest.keywordMisspellings = keywordMissSpellings;
                    }
                }
                catch (ex) {
                    TBUtilities.Log(ex);
                }

                resolve(keywordTagRequest);
            }, null, { contentType: "application/json", dataType: 'json' });
        });
    };

    var buildClientRequestObject = function (html) {

        var clientName = parseInnerTubeClientName(html);

        var clientVersion = parseInnerTubeClientVersion(html);

        var gl = parseInnerTubeContextCountry(html);

        var hl = parseInnerTubeContextLanguage(html);

        var clientObject = {
            "clientName": clientName,
            "clientVersion": clientVersion,
            "hl": hl,
            "gl": gl,
            "experimentsToken": ""
        };

        return clientObject;

    };

    var buildContextRequestObject = function (html) {

        var delegatedSessionId = parseDelegatedSessionId(html);

        var context = {
            "client": buildClientRequestObject(html),
            "request": {
                "returnLogEntry": true
            },
            "user": {
                "onBehalfOfUser": delegatedSessionId
            },
            "clientScreenNonce": ''
        };

        if (delegatedSessionId) {
            lscache.set(TBUtilities.DelegatedSessionIdKey(), delegatedSessionId, 15);
        }
    
        // For special new type of Channel Managers
        if (html.indexOf('CREATOR_CHANNEL_ROLE_TYPE_MANAGER') > -1) {
            context.user.delegationContext = {
                'externalChannelId': TBGlobal.CurrentChannelId(),
                    'roleType': {
                        channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_MANAGER'
                }
            };
            context.user.serializedDelegationContext = parseSerializedDelegationContext(html);
        }
        // For special new type of Channel owners
        else if (html.indexOf('CREATOR_CHANNEL_ROLE_TYPE_OWNER') > -1) {
            context.user.delegationContext = {
                'externalChannelId': TBGlobal.CurrentChannelId(),
                'roleType': {
                    channelRoleType: 'CREATOR_CHANNEL_ROLE_TYPE_OWNER'
                }
            };
            context.user.serializedDelegationContext = parseSerializedDelegationContext(html);
        }


        return context;
    };

    var convertImagesToDataUrls = function (images) {

        var imageConversions = [];
        var completedCount = 0;

        return new Promise(function (resolve, reject) {
            if (images.length > 0) {
                images.forEach(function (imageUrl) {
                    var apiUrl = TBGlobal.host + TBGlobal.apiUrls.imageGetBytes + '?url=' + encodeURIComponent(imageUrl);
                    TBExtension.Get(apiUrl, function (success, data, response) {
                        completedCount++;
                        if (success === true) {
                            var base64 = JSON.parse(response).split(',')[1];
                            var conversion = { 'url': data, dataUrl: base64 };
                            imageConversions.push(conversion);
                        }
                        else {
                            reject('error converting image to data url = ' + data);
                        }

                        if (completedCount >= images.length) {
                            resolve(imageConversions);
                        }

                    }, imageUrl);
                });
            }
            else {
                resolve(imageConversions);
            }
        });
    };


    var checkCardStartMs = function (cardClone, sourceVideoMiliseconds, destinationVideoMiliseconds) {

        var teaserStartMsInt = parseInt(cardClone.teaserStartMs);

        TBUtilities.Log('TubeBuddyCards - start ms =' + teaserStartMsInt);

        TBUtilities.Log('TubeBuddyCards - sourceVideoMiliseconds =' + sourceVideoMiliseconds);

        TBUtilities.Log('TubeBuddyCards - destinationVideoMiliseconds =' + destinationVideoMiliseconds);

        //end card video check
        // Dock at start if starts in last 15% of video
        var dockAtEnd = teaserStartMsInt > (sourceVideoMiliseconds * .85);
        var distanceFromEnd = sourceVideoMiliseconds - teaserStartMsInt;
        if (distanceFromEnd < 3000)
            distanceFromEnd = 3000;

        //if we dock at end destination - distance from end
        if (dockAtEnd) {
            teaserStartMsInt = destinationVideoMiliseconds - distanceFromEnd;
            TBUtilities.Log('TubeBuddyCards - dock at end = true, new teaserStartMs =' + teaserStartMsInt);
        }

        //if the start is greater than the video length, add it to the end minus one second
        if (teaserStartMsInt >= destinationVideoMiliseconds) {
            teaserStartMsInt = destinationVideoMiliseconds - 3000;
            TBUtilities.Log('TubeBuddyCards - start ms is greater than video length, adjusting to =' + teaserStartMsInt);
        }

        if (teaserStartMsInt < 0) {
            teaserStartMsInt = 0;
        }

        cardClone.teaserStartMs = teaserStartMsInt.toString();
    };

    var adjustEndScreenElementTiming = function (elementClone, sourceVideoEndscreenData, destinationVideoEndscreenData) {

        let lengthOfDestinationEndscreen = destinationVideoEndscreenData.videoDurationMs - destinationVideoEndscreenData.endscreenStartMs;

        if (elementClone.durationMs > lengthOfDestinationEndscreen || (elementClone.durationMs + elementClone.offsetMs) > lengthOfDestinationEndscreen) {


            let lengthOfSourceEndscreen = sourceVideoEndscreenData.videoDurationMs - sourceVideoEndscreenData.endscreenStartMs;
            let startOfModifiedEndscreen = destinationVideoEndscreenData.videoDurationMs - lengthOfSourceEndscreen;
            let lengthOfModfiedEndscreen = destinationVideoEndscreenData.videoDurationMs - startOfModifiedEndscreen

            if (destinationVideoEndscreenData.videoDurationMs > 25000 && destinationVideoEndscreenData.videoDurationMs <= 40000) {
                //elements longer than endscreen max, fill endscreen
                elementClone.durationMs = lengthOfDestinationEndscreen;
                elementClone.offsetMs = 0;
            }
            else {
                destinationVideoEndscreenData.endscreenStartMs = startOfModifiedEndscreen;
            }
        }

    };

    var validateEndscreenElements = function (elements) {

        let error = '';

        let videoMostRecentCount = 0;
        let videoViewerCount = 0;
        let subscribeCount = 0;
        let linkCount = 0;

        if (elements.length > 4) {
            error += "<p>Max of 4 endscreen elements reached</p>";
        }

        //foreach element remove any extra object properties
        jQuery.each(elements, function (i, element) {

            switch (element.type) {

                case 'CHANNEL':
                    {
                        if (element.channelEndscreenElement.isSubscribe) {
                            subscribeCount++;
                        }
                        break;
                    }
                case 'WEBSITE':
                    {
                        linkCount++;
                        break;
                    }
                case 'VIDEO':
                    {
                        switch (element.videoEndscreenElement.videoType) {
                            case 'VIDEO_TYPE_RECENT_UPLOAD': {
                                videoMostRecentCount++;
                                break;
                            }
                            case 'VIDEO_TYPE_BEST_FOR_VIEWER': {
                                videoViewerCount++;
                                break;
                            }
                        }

                        break;
                    }
            }
        });

        if (videoMostRecentCount > 1) {
            error += "<p>Can only have 1 most recent video</p>";
        }

        if (videoViewerCount > 1) {
            error += "<p>Can only have 1 best for viewer</p>";
        }

        if (subscribeCount > 1) {
            error += "<p>Can only have 1 subscribe</p>";
        }

        if (linkCount > 1) {
            error += "<p>Can only have 1 link</p>";
        }

        return error;
    };

    var adjustEndScreenTiming = function (sourceVideoDurationAndEndscreen, destinationVideoDurationAndEndscreen) {

        //if the destination video doesn't have any endscreens, use the source as the template for timing
        let endScreenStart = destinationVideoDurationAndEndscreen.endscreenStartMs;
        if (endScreenStart === null) {
            //shorter videos
            if (destinationVideoDurationAndEndscreen.videoDurationMs > 25000 && destinationVideoDurationAndEndscreen.videoDurationMs <= 31000) {
                endScreenStart = destinationVideoDurationAndEndscreen.videoDurationMs - 10000;
            }
            //else if (destinationVideoDurationAndEndscreen.videoDurationMs > 25000 && destinationVideoDurationAndEndscreen.videoDurationMs <= 40000) {
            //    endScreenStart = destinationVideoDurationAndEndscreen.videoDurationMs - 15000;
            //}
            else {
                let lengthOfSourceEndscreen = sourceVideoDurationAndEndscreen.videoDurationMs - sourceVideoDurationAndEndscreen.endscreenStartMs;
                endScreenStart = destinationVideoDurationAndEndscreen.videoDurationMs - lengthOfSourceEndscreen;
            }

        }

        return endScreenStart;
    };

    var getDateFromAnalyticsTimeStamp = function timeConverter(UNIX_timestamp) {
        var a = new Date(UNIX_timestamp / 1000);
        var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
        var year = a.getFullYear();
        var month = months[a.getMonth()];
        var date = a.getDate();
        var hour = a.getHours();
        var min = a.getMinutes();
        var sec = a.getSeconds();
        var dt = year + '-' + (a.getMonth() + 1) + '-' + date;
        return dt;
    }

    var capitlizeFirstLetterString = function (word) {

        return word.charAt(0).toUpperCase() + word.slice(1);

    };

    var lowerCaseFirstLetterString = function (word) {

        return word.charAt(0).toLowerCase() + word.slice(1);

    };

    var parseFacebookLikeCountText = function (htmlResponse) {
        
        if (!htmlResponse)
            return 'error';
        var sanitizedHtml = DOMPurify.sanitize(htmlResponse, DOMPurifyConfig);
        
        var likeCountText = jQuery(sanitizedHtml).find('span[id^="u_0_0"]').text();
        
        return likeCountText;
    };

    var createSha256 = async function (string) {
        const encoder = new TextEncoder();
        const data = encoder.encode(string);
        const hash = await crypto.subtle.digest('SHA-256', data);
        const hashString = btoa(Array.prototype.map.call(new Uint8Array(hash), char => String.fromCharCode(char)).join(''));
        return hashString;
    };

    var reportExtensionDOMIssue = async function (moduleName, failingSelector, page) {
        var hashString = moduleName + failingSelector + (page ? page : '');
        var hash = await createSha256(hashString);
        return new Promise(function (resolve) {
            
            TBExtension.GetDbValue('tbReportedExtensionDOMIssues', function (key, value) {
                var reportedIssues = [];
                if (value) {
                    reportedIssues = value.split(',');
                    if (reportedIssues.indexOf(hash) > -1) {
                        //already reported   
                        TBUtilities.Log('DOM Issue already reported. Module: ' + moduleName + ' Selector: ' + failingSelector);
                        resolve();
                        return;
                    }
                }

                reportedIssues.push(hash);
                TBUtilities.Log('Reporting new DOM issue. Module: ' + moduleName + ' Selector: ' + failingSelector);
                TBExtension.SetDbValue('tbReportedExtensionDOMIssues', reportedIssues.join(','));                

                //check if we need to capture the page html & details
                var detailsCheckPostData = {
                    Hash: hash,
                    ModuleName: moduleName,
                    Selector: failingSelector,
                    PageURL: location.href
                };

                var detailsCheckApiUrl = TBGlobal.host + TBGlobal.apiUrls.checkYTDOMIssue;
                TBExtension.Post(detailsCheckApiUrl, JSON.stringify(detailsCheckPostData), function (success, data, response) {
                    //default to empty string so we're not sending full page html unless needed
                    var htmlElement = '';
                    try {
                        if (success === true && response == "true") {
                            //only send full page html if server says we need more
                            htmlElement = document.querySelector('html').outerHTML;                             
                        }

                        var postData = {
                            Hash: hash,
                            ModuleName: moduleName,
                            Selector: failingSelector,
                            PageURL: location.href,
                            ChannelId: TBGlobal.CurrentChannelId(),
                            UserAgent: navigator.userAgent,
                            PageHTML: htmlElement,
                            Country: '',
                            Language: navigator.language,
                            ExtVersion: TBExtension.GetExtensionVersion()
                        }

                        var apiUrl = TBGlobal.host + TBGlobal.apiUrls.reportYTDOMIssue;
                        TBExtension.Post(apiUrl, JSON.stringify(postData), function (success, data, response) {
                            try {
                                if (success === true) {
                                    TBUtilities.Log('Successfully reported YT DOM Issue');
                                } else {
                                    TBUtilities.Log('Error reporting YT DOM Issue');
                                }
                            } catch (ex) {
                                TBUtilities.Log(ex);
                            } finally {
                                resolve();
                            }
                        }, null, { contentType: "application/json; charset=UTF-8" });

                    } catch (ex) {
                        TBUtilities.Log(ex);
                    }
                }, null, { contentType: "application/json; charset=UTF-8" });                        
            }); 
        });
    };

    return {

        // Create
        // Get
        // Remove
        // Validate
        // Format
        // Perform
        // Add 

        ActionTypes: {

            Unknown: 0,

            AdvancedVideoEmbedding_Use: 100,

            AnimatedGifGenerator_Generate: 200,
            AnimatedGifGenerator_Share: 201,

            AnnotationTemplates_Applied: 300,

            BestTimeToPublish_Calculate: 400,

            BrandAlerts_Fetch: 500,

            BulkCopyAnnotations_Copy: 600,

            BulkCopyCards_Copy: 700,

            BulkDeleteAnnotations_Delete: 800,

            BulkDeleteCards_Delete: 900,

            BulkFindReplaceAppent_Find: 1000,
            BulkFindReplaceAppend_FindAndUpdate: 1001,

            BulkPublishToFacebook_Publish: 1100,

            BulkThumbnailOverlays_Add: 1200,
            BulkThumbnailOverlays_Remove: 1201,

            BulkUpdateAnnotations_Update: 1300,

            BulkUpdateCards_Update: 1400,

            CannedResponses_Use: 1500,

            CaptionsService_Order: 1600,

            CardTemplates_Apply: 1700,

            ChannelBackup_Backup: 1800,

            Channelytics_Use: 1900,

            CommentFilters_ApplyFilter: 2000,

            CommunityConnect_External: 2100,
            CommunityConnect_Internal: 2101,

            CompetitorsScorecard_View: 2200,
            CompetitorsUploadAlert_Fetch: 2210,

            DefaultUploadProfiles_Apply: 2300,

            DescriptionPromotion_Promote: 2400,

            FeaturedVideoPromotions_Promote: 2500,

            HealthReport_View: 2600,

            LanguageAnalysis_Use: 2700,

            PickAWinner_Pick: 2800,

            PublishToFacebook_Publish: 2900,

            QuickEditToolbar_PlaylistEdit: 2970,
            QuickEditToolbar_MyVideosEdit: 2971,
            QuickEditToolbar_VideoClick: 2972,

            QuickLinksMenu_Click: 3000,

            ScheduleGoLive_Schedule: 3100,
            ScheduleGoLive_AddCommentAfterPublish: 3101,

            SearchRanking_View: 3200,

            ShareTracker_Track: 3300,

            SocialMonitor_View: 3400,

            SubscriberExport_Export: 3500,

            SubscriberOutreach_ViewSocial: 3600,

            SuggestedTags_Use: 3700,

            SunsetVideos_Schedule: 3800,

            TagExplorer_TagExplored: 3900,

            TagSorter_Sorted: 4000,

            AutoTranslator_Use: 4100,

            ThumbnailGenerator_Use: 4200,

            VideoAbTests_Use: 4300,

            VideoTopicPlanner_SaveComment: 4400,
            VideoTopicPlanner_Use: 4401,

            ViewAndCopyVideoTags_Copy: 4500,

            WatchPageStats_Stats: 4600,

            BulkCopyEndScreen_Copy: 4700,

            Playlists_BulkUpdate: 4800,

            Playlist_UpdateOrdering: 4810,
            Playlist_UpdatePrivacy: 4820,
            Playlist_UpdateFiltering: 4830,

            UploadChecklist_CompletedItem: 4900,

            BestPractice_UserCompleted: 5000,

            Opportunities_MarkedAsComplete: 5100,
            Opportunities_UserCompleted: 5101,

            NotificationMenu_PostCommentReply: 5200,

            CommentCloud_Show: 5300,

            EndScreenTemplates_Apply: 5400,

            SocialBluebookValuation_GetValue: 5500,

            BulkUpdateEndScreens_Update: 5600,

            ExportComments_Export: 5700,

            TagLists_Export: 5800,

            CopyTo_Export: 5900,

            SearchTermAnalysis_Use: 7100,

            InstaSuggest_Use: 7200,

            SEOStudio_Save: 7300,
            SEOStudio_ApplyToVideo: 7301,

            Strategy_Start: 7400,
            Strategy_Finish: 7401,

            ClickMagnet_AdvancedAnalytics: 7500,
            ClickMagnet_ElementInspector: 7501,
            ClickMagnet_CTROppotunitiesUse: 7502,
            ClickMagnet_PowerRankings: 7503,

            SearchInsights_ViewDetails: 7600

        },

        ActivityLogTypes: {

            Unknown: 0,

            QuickLinksMenu_MyAccount: 1,
            QuickLinksMenu_HealthReport: 2,
            QuickLinksMenu_CompetitorScorecard: 3,
            QuickLinksMenu_BrandAlerts: 4,
            QuickLinksMenu_ABTests: 5,
            QuickLinksMenu_BackupsAndExports: 6,
            QuickLinksMenu_CannedResponses: 7,
            QuickLinksMenu_Settings: 8,
            QuickLinksMenu_CommunityForums: 9,
            QuickLinksMenu_TipsAndTricks: 10,
            QuickLinksMenu_SuggestAFeature: 11,
            QuickLinksMenu_KnowledgeBase: 12,
            QuickLinksMenu_Dashboard: 13,
            QuickLinksMenu_MyVideos: 14,
            QuickLinksMenu_Playlists: 15,
            QuickLinksMenu_LiveStreaming: 16,
            QuickLinksMenu_Comments: 17,
            QuickLinksMenu_Messages: 18,
            QuickLinksMenu_Subscribers: 19,
            QuickLinksMenu_Analytics: 20,
            QuickLinksMenu_Realtime: 21,
            QuickLinksMenu_TagExplorer: 22,
            QuickLinksMenu_VideoTopicPlanner: 23,
            QuickLinksMenu_BestTimeToPublish: 24,

            PublishToFacebook_Blocked: 30,

            BulkPublishToFacebook_UpgradeClick: 40,

            ShowUpgradePage: 50,

            Layout_SubmitDebugCode: 60,
        },

        GetActivityLogTypeValueFromString: function (name) {
            for (var prop in TBUtilities.ActivityLogTypes) {
                if (prop == name) {
                    return TBUtilities.ActivityLogTypes[prop];
                }
            }

            return 0;
        },

        AddDebug: function (msg) {
            addDebug(msg);
        },

        AddDebugObject: function (obj) {
            addDebugObject(obj);
        },

        CreateCookie: function (name, value, days) {
            createCookie(name, value, days);
        },

        GetCookie: function (name) {
            return getCookie(name);
        },

        RemoveCookie: function (name) {
            createCookie(name, "", -1);
        },

        ReplaceAll: function (string, find, replace, ignoreCase, wholeWord) {
            return replaceAll(string, find, replace, ignoreCase, wholeWord);
        },


        CountMatches: function (string, find, ignoreCase, wholeWord, wholePhrase) {
            return countMatches(string, find, ignoreCase, wholeWord, wholePhrase);
        },

        GetInArray: function (inArray, prop, value) {
            return getInArray(inArray, prop, value);
        },

        RemoveFromArray: function (inArray, prop, value) {
            return removeFromArray(inArray, prop, value);
        },

        SortArray: function (inArray, prop, decending, isNumber) {
            return sortArray(inArray, prop, decending, isNumber);
        },

        SortArrayTwoNumberColumns: function (inArray, prop1, decending1, prop2, decending2) {
            return sortArrayTwoNumberColumns(inArray, prop1, decending1, prop2, decending2);
        },

        CreateGuid: function () {
            return createGuid();
        },

        ValidateEmail: function (email) {
            return validateEmail(email);
        },

        GetHtmlEncoded: function (str) {
            return getHtmlEncoded(str);
        },

        GetXmlEncoded: function (str) {
            return getXmlEncoded(str);
        },

        GetUrlEncoded: function (str) {
            return getUrlEncoded(str);
        },

        HighlightText: function (text, innerHTML, className) {
            return highlightText(text, innerHTML, className);
        },

        GetUrlDecoded: function (str) {
            return getUrlDecoded(str);
        },
        FormatNumberWithCommas: function (number) {
            return formatNumberWithCommas(number);
        },

        FormatNumberAbbreviated: function (number, decimals, abbrevClass) {
            return formatNumberAbbreviated(number, decimals, abbrevClass);
        },

        FormatCurrency: function (n, c, d, t) {
            return formatCurrency(n, c, d, t);
        },

        PerformGet: function (url, successCallback, faliureCallback) {
            performGet(url, successCallback, faliureCallback);
        },

        PerformPost: function (url, data, successCallback, faliureCallback) {
            performPost(url, data, successCallback, faliureCallback);
        },

        GetEmailsWithinString: function (inString) {
            return getEmailsWithinString(inString);
        },

        AddAction: function (data, actionType, toolId) {
            addAction(data, actionType, toolId);
        },

        AddActivityLog: addActivityLog,

        LogError: function (details) {
            logError(details);
        },

        LogFailedWebRequest: logFailedWebRequest,

        UnicodeToChar: function (text) {
            return unicodeToChar(text);
        },

        FormatFileName: function (text) {
            return formatFileName(text);
        },

        GetOs: function () {
            return getOs();
        },

        GetUrlParameter: function (param) {
            return getUrlParameter(param);
        },

        ToLongDateTimeString: function (date) {
            return toLongDateTimeString(date);
        },

        ToShortDateString: function (date) {
            return toShortDateString(date);
        },

        ToCurrency: function (number, dollarClass) {
            return toCurrency(number, dollarClass);
        },

        GetTime: function () {
            return getTime();
        },

        Log: function (msg) {
            return log(msg);
        },

        MilliSecondsToTime: function (milli, round) {
            return milliSecondsToTime(milli, round);
        },

        MilliSecondsToTimeShort: function (milli) {
            return milliSecondsToTimeShort(milli);
        },

        AutoIncrementCount: function (element, speed) {
            autoIncrementCount(element, speed);
        },

        LoadExternalFile: function (filename, filetype) {
            loadExternalFile(filename, filetype);
        },

        IsAlphaNumeric: function (string) {
            return isAlphaNumeric(string);
        },

        Truncate: function (str, n, useWordBoundary) {
            return truncate(str, n, useWordBoundary);
        },

        GetDebugEntries: function () {
            return getDebugEntries();
        },

        ValidateUrl: function (url) {
            return validateUrl(url);
        },

        IsNullOrEmptyOrWhitespace: function (msg) {
            return isNullOrEmptyOrWhitespace(msg);
        },

        GetVideoTweetCount: function (videoId, callback) {

            return getVideoTweetCount(videoId, callback);
        },

        Alert: function (msg, action) {
            tbAlert(msg, action);
        },

        Confirm: function (msg, yesAction, noAction) {
            return tbConfirm(msg, yesAction, noAction);
        },

        Prompt: function (msg, value, onok, oncancel) {
            tbPrompt(msg, value, onok, oncancel);
        },

        ParseChannelIdFromUrl: function (url) {
            return parseChannelIdFromUrl(url);
        },

        ParseVideoIdFromUrl: function (url) {
            return parseVideoIdFromUrl(url);
        },

        ParsePlaylistIdFromUrl: function (url) {
            return parsePlaylistIdFromUrl(url);
        },

        UpdateUrlParameter: function (uri, key, value) {
            return updateUrlParameter(uri, key, value);
        },

        DataURItoBlob: function (dataUri) {
            return dataURItoBlob(dataUri);
        },

        ForceDebugging: function () {
            debugLocal = true;
        },

        FixLocalHtml: function (localHtml) {
            return fixLocalHtml(localHtml);
        },

        GetParameterByName: function (name, url) {
            return getParameterByName(name, url);
        },

        ParseInnerTubeApiKey: function (html) {
            return parseInnerTubeApiKey(html);
        },

        ParseDelegatedSessionId: function (html) {
            return parseDelegatedSessionId(html);
        },
        ParseInnerTubeContext: function (html) {
            return parseInnerTubeContext(html);
        },

        ParseInnerTubeApiClientVersion: function (html) {
            return parseInnerTubeClientVersion(html);
        },

        ParseInnerTubeApiClientName: function (html) {
            return parseInnerTubeClientName(html);
        },

        ParseInnerTubeContextCountry: function (html) {
            return parseInnerTubeContextCountry(html);
        },

        GetSessionToken: getSessionToken,

        GetCommentTokenFromWatchPageHtml: function (html) {
            return getCommetTokenFromWatchPageHtml(html);
        },

        GetIdokenFromWatchPageHtml: function (html) {
            return getIdTokenFromWatchPageHtml(html);
        },

        GetYouTubeUIVersion: function (html) {
            return getYouTubeUIVersionNumber(html);
        },

        FixedEncodeURIComponent: function (str) {
            return fixedEncodeURIComponent(str);
        },

        IsWordCensored: function (str) {
            return isWordCensored(str);
        },

        GetCensoredWords: function () {
            return getCensoredWords();
        },

        GetYouTubeAutoSuggested: function (term, callback) {
            return getYouTubeAutoSuggested(term, callback);
        },

        GetTubeBuddyCheckboxHtml: function (value, classes, attributes) {
            return getTubeBuddyCheckboxHtml(value, classes, attributes);
        },

        GetTagCharacterCount: function (tags) {
            return getTagCharacterCount(tags);
        },

        CorbUrl: function (url) {
            return corbUrl(url);
        },

        IsResponseContextFound: function (html) {
            return isResponseContextFound(html);
        },

        ParseResponseContextFromHtml: function (html) {
            return parseResponseContextFromHtml(html);
        },

        ParseVideoEditObjectFromHtml: function (html) {
            return parseVideoEditObjectFromHtml(html);
        },

        GetVideoSearchCollection: function (sectionListRendererContents) {
            return getVideoSearchCollection(sectionListRendererContents);
        },

        GetRichGridRendererContents: function (renderer) {
            return getRichGridRendererContents(renderer);
        },

        GetSectionListRendererContents: function (renderer) {
            return getSectionListRendererContents(renderer);
        },

        GetVideoSearchCollectionIndex: function (sectionListRendererContents) {
            return getVideoSearchCollectionIndex(sectionListRendererContents);
        },

        GetListRendererKey: function (primaryContents) {
            return getListRenderKey(primaryContents);
        },

        GetSectionRendererKey: function (contents) {
            return getSectionRendererKey(contents);
        },

        ParsePlayerResponseFromHtml: function (html) {
            return parsePlayerResponseFromHtml(html);
        },

        GetKeywordRecommendedTags: function (keyword) {
            return getKeywordRecommendedTags(keyword);
        },

        BuildContextRequestObject: function (html) {
            return buildContextRequestObject(html);
        },

        ProcessHeader: function (params) {
            return processHeader(params);
        },

        ConvertImagesToDataUrls: function (images) {
            return convertImagesToDataUrls(images);
        },

        AdjustEndScreenElementTiming: function (elementClone, sourceVideoEndScreenData, destVideoEndScreenData) {
            return adjustEndScreenElementTiming(elementClone, sourceVideoEndScreenData, destVideoEndScreenData);
        },

        AdjustEndScreenTiming: function (template, destination) {
            return adjustEndScreenTiming(template, destination);
        },

        ValidateEndscreenElements: function (elements) {
            return validateEndscreenElements(elements);
        },

        GetVideoTopics: function (keyword) {
            return getVideoTopics(keyword);
        },

        DownloadArrayToCSV: function (dataArray, fileName) {
            downloadArrayToCSV(dataArray, fileName);
        },

        CapitlizeFirstLetterString: function (word) {
            return capitlizeFirstLetterString(word);
        },

        LowerCaseFirstLetterString: function (word) {
            return lowerCaseFirstLetterString(word);
        },

        HeaderCacheKey: function () {
            return _headerCacheKey;
        },

        DelegatedSessionIdKey: function () {
            return _delegatedSessionId;
        },

        GetDateFromAnalyticsTimeStamp: function (date) {
            return getDateFromAnalyticsTimeStamp(date);
        },

        CheckCardStartMs: function (card, totalVideoMiliseconds, destinationVideoMiliseconds) {

            checkCardStartMs(card, totalVideoMiliseconds, destinationVideoMiliseconds);

        },

        GetTimeZoneOffSetInHours: function (name) {
            return getTimeZoneOffSetInHours(name);
        },

        ParseFacebookLikeCountText: function (htmlResponse) {
            return parseFacebookLikeCountText(htmlResponse);
        },

        ReportExtensionDOMIssue: function (moduleName, failingSelector, page) {
            return reportExtensionDOMIssue(moduleName, failingSelector, page);
        },
    };

})();

//#endregion

// Prototypes
if (!String.prototype.padLeft) {
    String.prototype.padLeft = function (totalLength, padCharacter) {
        var s = this, padCharacter = padCharacter || '0';
        while (s.length < totalLength) s = padCharacter + s;
        return s;
    }
}

if (!String.prototype.hashCode) {
    String.prototype.hashCode = function () {
        var hash = 0, i, chr;
        if (this.length === 0) return hash;
        for (i = 0; i < this.length; i++) {
            chr = this.charCodeAt(i);
            hash = ((hash << 5) - hash) + chr;
            hash |= 0; // Convert to 32bit integer
        }
        return hash;
    };
}

var tbdelay = (function () {
    var timer = 0;
    return function (callback, ms) {
        clearTimeout(timer);
        timer = setTimeout(callback, ms);
    };
})();

(function ($) {

    // jQuery on an empty object, we are going to use this as our Queue
    var ajaxQueue = $({});

    $.ajaxQueue = function (ajaxOpts) {
        var jqXHR,
            dfd = $.Deferred(),
            promise = dfd.promise();

        // run the actual query
        function doRequest(next) {

            // NOTE: If we ever need to use this for a cross domain call on FireFox
            // this implementation will have to be updated to use the Fetch API in that
            // case specifically.

            jqXHR = $.ajax(ajaxOpts);
            jqXHR.done(dfd.resolve)
                .fail(dfd.reject)
                .then(next, next);
        }

        // queue our ajax request
        ajaxQueue.queue(doRequest);

        // add the abort method
        promise.abort = function (statusText) {

            // proxy abort to the jqXHR if it is active
            if (jqXHR) {
                return jqXHR.abort(statusText);
            }

            // if there wasn't already a jqXHR we need to remove from queue
            var queue = ajaxQueue.queue(),
                index = $.inArray(doRequest, queue);

            if (index > -1) {
                queue.splice(index, 1);
            }

            // and then reject the deferred
            dfd.rejectWith(ajaxOpts.context || ajaxOpts, [promise, statusText, ""]);
            return promise;
        };

        return promise;
    };

})(jQuery);
;
/// <reference path="../general/tubebuddyUtilities.js" />
/// <reference path="../general/tubebuddyYouTubeApi.js" />

/// <reference path="../SubscribersList/subscribersList.js" />
/// <reference path="../SubscribersExport/subscribersExport.js" />
/// <reference path="../v2vpromotion/v2vpromotion.js" />
/// <reference path="../VideosList/videosList.js" />
/// <reference path="../EditVideo/editVideo.js" />
/// <reference path="../EditVideoStudio/editVideoStudio.js" />
/// <reference path="../shared/YouTubeActions.js" />
/// <reference path="../KeywordExplorer/KeywordExplorer.js" />
/// <reference path="../Upload/upload.js" />
/// <reference path="../UploadDefaults/UploadDefaults.js" />
/// <reference path="../VideoInfo/videoInfo.js" />
/// <reference path="../shareTracker/shareTracker.js" />
/// <reference path="../Screenshot/Screenshot.js" />
/// <reference path="../SocialBluebook/SocialBluebook.js" />
/// <reference path="../EditVideoStudio/metaAutoTranslator.js"/>


//#region javascript module 

var TBGlobal = (function () {


    var _currentChannelId = '';
    var _currentToken = '';
    var _initializedDate;
    var _locTemp = '';

    var _profile = null;

    var _googleAuthUser = 0;

    var _modules = [];
    var _modulesLoaded = false;

    var _profileUpdatedListeners = [];

    var _userDebugCode = '';

    var _networks = [];

    var fixUpSafariTmp = [];

    var _diagnostics = '{S}';

    var _youTubeUI = null;

    var $target = null;

    /* Page list
        General Page
        Search Results
        Edit Video
        Watch Video
        Upload Defaults
        Channel Page
        Video Upload
        Comments
        My Videos
        Subscribers
        Playlists
        Cards
    */

    var _toolList = [
        {
            id: 'bulkcopycards',
            icon: 'copy',
            category: 'bulk',
            color: '#445569',
            pages: ['My Videos'],
            name: 'Bulk Copy Cards',
            summary: 'Automate the process of copying Cards across some or all of your videos',
            howToVideoId: 'llMep6_-ijY',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Manually adding Cards on all your videos one by one takes forever and is a pain in the &#^@@$',
                'Copying Cards through TubeBuddy ensures you have a clean, consistent look across all your videos',
                '<a href="https://support.google.com/youtube/answer/6140493?hl=en&ref_topic=6140492" target="_blank">Cards</a> are extremely important for marketing your brand on desktop and mobile'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Bulk Copy Cards" from the TubeBuddy menu next to the video that you want to copy Cards <i>from</i>.<br /><img src="/assets/images/tools/screenshots/bulkcopycardswhere.png" />',
            tipsAndTricks: [
                'Don\'t worry if your videos have different durations. TubeBuddy uses "Smart Timing" to figure out where to place the Cards on each destination video. For example, if you have a set of <i>end cards</i>, TubeBuddy automatically places them at the end of each destination video (same distance from end as source video).',
                'If you are only using this tool for your most recent upload, use <a href="Tools?tool=cardtemplates">Card Templates</a> instead.',
                'If want to get rid of all existing Cards from the destination videos before copying the new Cards, simply select the "remove existing cards" option in Step 2.',
                'Each video is processed 1 at a time and takes a second or two. So 1,000 videos might take about 30 minutes to process. We recommend limiting bulk copying to 2,000 or less videos. If you have more, consider breaking them up into playlists and running the tool for each playlist separately.',
                'After processing is complete, you can click on the video link in the <i>Completed</i> area to quickly verify that it was done correctly.',
                'You can customize Card properties on individual destination videos in Step 3.',
                'TubeBuddy handles all Card types and each time YouTube puts out a new type of Card, it is added to our system within a day or two.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot copy Cards to more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'bulkdeletecards',
            icon: 'remove',
            category: 'bulk',
            color: '#445569',
            pages: ['My Videos'],
            name: 'Bulk Delete Cards',
            summary: 'Delete Cards from some or all of your videos with just a few clicks',
            howToVideoId: '8D1HVZSLdNE',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Manually deleting Cards on all your videos one by one would take forever',
                'It\'s an easy way to create a clean slate before executing a <a href="https://support.google.com/youtube/answer/6140493?hl=en&ref_topic=6140492" target="_blank">Cards</a> strategy',
                'Quickly remove Cards from an old or expired promotion'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Delete Cards" from the Bulk TubeBuddy menu above your list of videos.<br /><img src="/assets/images/tools/screenshots/bulkdeletecardswhere.png" />',
            tipsAndTricks: [
                'The Bulk Copy Cards tool has an option to delete all Cards on videos before copying new Cards. In some cases it makes sense to use that option instead of this tool.',
                'Remember this process deletes ALL Cards from a video. (Some day we will add the ability to delete just certain types of Cards)',
                'Each video is processed 1 at a time and takes a second or two. So 1,000 videos might take about 30 minutes to process. We recommend limiting bulk copying to 2,000 or less videos. If you have more, consider breaking them up into playlists and running the tool for each playlist separately.',
                'After processing is complete, you can click on the video link in the <i>Completed</i> area to quickly verify that it was done correctly.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot delete Cards from more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'sunsetvideos',
            icon: 'sun-o',
            category: 'productivity',
            color: '#445569',
            name: 'Sunset Videos',
            pages: ['My Videos', 'Edit Video', 'Upload'],
            summary: 'Schedule videos to be hidden and removed from playlists at a future date/time ',
            howToVideoId: 'z_GVn_q-o30',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You need to set a video\'s Privacy to Private or Unlisted at a certain time in the future',
                'You want to remove a video from some or all your playlists at a certain time in the future',
                'Protect yourself against licensed content'],
            whereToFindIt: 'On the My Videos page, Upload Page or Edit Video page. <br /><img src="/assets/images/tools/screenshots/sunsetvideowhere.png" />',
            tipsAndTricks: [
                'You will receive a notification 24 hours in advance before items scheduled to be sunsetted are actually sunsetted.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Star | Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        //{
        //    id: 'navwindowcomments',
        //    icon: 'comments-o',
        //    category: 'productivity',
        //    color: '#445569',
        //    name: 'Notification Commenting',
        //    pages: [''],
        //    summary: 'Reply to comments directly from the YouTube notification window',
        //    howToVideoId: 'OpYocrIanj0',
        //    Level0Limitation: null,
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: true,
        //    Level1Access: true,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'You want to comment on videos directly from the YouTube notification window',
        //        'You don\'t want to leave the current page you are on to make a comment',
        //        'You are frustrated that YouTube removed this basic functionality'],
        //    whereToFindIt: 'Every YouTube Page',
        //    tipsAndTricks: [
        //        ''],
        //    limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        //},
        {
            id: 'playlistactions',
            icon: 'bolt',
            category: 'productivity',
            color: '#445569',
            name: 'Playlist Actions',
            pages: ['All Playlists'],
            summary: 'Advanced ordering and filtering options for videos within playlists',
            howToVideoId: '6wmaBD5pVq8',
            Level0Limitation: 'Can only use Standard / Misc Ordering options',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to order a playlist based on video Engagement, Subscribers Gained or Watch Time',
                'You want to randomize, alphabetize or reverse the order of videos in a playlist',
                'You want to remove Private or Unlisted videos from a playlist'],
            whereToFindIt: '<a href="https://www.youtube.com/view_all_playlists" target="_blank">All Playlists Page</a>',
            tipsAndTricks: [
                'Sort your playlist by Watch Time in order to gain the best SEO boost'],
            //limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        //{
        //    id: 'bulkupdateplaylists',
        //    icon: 'refresh',
        //    category: 'bulk',
        //    color: '#445569',
        //    name: 'Bulk Update Playlists',
        //    pages: ['All Playlists'],
        //    summary: 'Update Privacy, Ordering and Filtering settings across some or all your Playlists at once',
        //    howToVideoId: 'fNh6kh8NGPU',
        //    Level0Limitation: '',
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: false,
        //    Level1Access: false,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'You want to set a group of playlists to Public or Private',
        //        'You want to change the order of videos within some or all your playlists',
        //        'You want to remove all Private and/or Unlisted videos quickly from all your playlists'],
        //    whereToFindIt: '<a href="https://www.youtube.com/view_all_playlists" target="_blank">All Playlists Page</a>',
        //    tipsAndTricks: [
        //        ''],
        //    limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        //},
        {
            id: 'bulkdemonetization',
            icon: 'dollar',
            category: 'bulk',
            color: '#445569',
            name: 'Demonetization Double-Check',
            pages: ['My Videos'],
            summary: 'Quickly find demonetized videos on your channel then bulk submit them for manual review',
            howToVideoId: 'BZvV9tCbyH8',
            Level0Limitation: '',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to find old demonetized videos that you are not aware of',
                'You want to submit videos for manual review in bulk',
                'You want to ensure you are making the most money possible from your channel'],
            whereToFindIt: '<img src="/assets/images/tools/screenshots/bulkdemonetizationwhere.png" /><a href="https://www.youtube.com/my_videos" target="_blank">My Videos Page</a>',
            tipsAndTricks: [
                'Run this process at least once per week to make sure you are not missing out on old videos that were demonetized.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'schedulegolive',
            icon: 'power-off',
            category: 'productivity',
            color: '#445569',
            name: 'Scheduled Publish',
            pages: ['My Videos', 'Edit Video', 'Upload'],
            summary: 'Schedule unlisted videos to be published and added to playlists at a future date/time ',
            howToVideoId: 'qK0dT2_uNS4',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to give your patron supporters early access to a video before making it public',
                'You want to add a video to playlists at a certain time in the future',
                'You want to pass around a video internally at your business before publishing it live'],
            whereToFindIt: 'On the My Videos page, Upload Page or Edit Video page. <br /><img src="/assets/images/tools/screenshots/scheduledpublishwhere.png" />',
            tipsAndTricks: [
                '(optional) You will receive a notification 24 hours in advance before items scheduled to be published are actually live.'],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot Scheduled Publish</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'bulkthumbnailoverlays',
            icon: 'photo',
            category: 'bulk',
            color: '#445569',
            name: 'Bulk Thumbnail Overlays',
            pages: ['My Videos'],
            summary: 'Add (and optionally remove later) a graphic overlay to some or all of your video thumbnails',
            howToVideoId: 'JHnVDCA27ZQ',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to add your business logo to all your thumbnails but don\'t have 500 hours of spare time to do it manually',
                'You want to run a promotion on your channel by adding (and later removing) a sponsor\'s logo to your videos\' thumbnails',
                'You want to add a distinct image overlay to all thumbnails in a certain playlist'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Thumbnail Overlays" from the Bulk TubeBuddy menu above your list of videos.<br /><img src="/assets/images/tools/screenshots/bulkthumbnailoverlayswhere.png" />',
            tipsAndTricks: [
                'This feature is coming soon! (we are just running some final tests)'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot update thumbnails of more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'bulkfindreplaceappend',
            icon: 'refresh',
            category: 'bulk',
            color: '#445569',
            name: 'Bulk Find, Replace &amp; Append',
            pages: ['My Videos'],
            summary: 'Find & Replace, Append or Remove from video titles and descriptions across your entire channel',
            howToVideoId: 'mU9tahNRVgE',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to insert a new social profile in the middle of all your existing videos\' descriptions',
                'You want to replace a hyperlink in your videos\' descriptions with a new link',
                'You want to remove references to that website you were promoting'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Titles & Descriptions" from the Bulk Meta Updates TubeBuddy menu above your list of videos.<br /><img src="/assets/images/tools/screenshots/bulkfindandreplacewhere.png" />',
            tipsAndTricks: [
                'Use caSE SensiTivE and whole word matches to ensure you only update the text you want.',
                'Preview changes before they go live by clicking on the search icon next to each video in step 3.',
                'Each video is processed 1 at a time and takes a second or two. So 1,000 videos might take about 30 minutes to process. We recommend limiting bulk copying to 2,000 or less videos. If you have more, consider breaking them up into playlists and running the tool for each playlist separately.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot run Find / Replace / Append on more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'viewandcopyvideotags',
            icon: 'copy',
            category: 'optimization',
            color: '#9bba5c',
            name: 'View and Copy Video Tags',
            pages: ['Watch Video'],
            summary: 'View and Copy video tags from any video (not just your own)',
            howToVideoId: 'C95gGD6nr6Q',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You\'re wondering what your competitor is tagging their videos with',
                'You\'re looking for ideas around what tags to use in your videos',
                'You want to see why particular videos are rankings higher in search results'],
            whereToFindIt: '<img src="/assets/images/tools/viewandcopytagswhere2.png" />',
            tipsAndTricks: [
                'You can hide this feature by going to your Settings tab in the main TubeBuddy menu next to the Upload button in YouTube.',
                'Click on any Tag in the list to run the Tag Explorer tool on the tag.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'emojipicker',
            icon: 'smile-o',
            category: 'productivity',
            color: '#9bba5c',
            name: 'Emoji Picker',
            pages: ['Edit Video'],
            summary: 'Add Emojis to your Titles, Tags and Descriptions',
            howToVideoId: 'vuGR5h4SC0k',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to make your Titles stand out in a crowd',
                'You want to tag your videos in unique ways to help in related videos section',
                'You don\'t want to have to visit an external site to use emojis'],
            whereToFindIt: 'Info and Settings + Upload Page for Videos',
            tipsAndTricks: [
                'Use the Search bar to quickly find what you\'re looking for.',
                'Use the Recently Used area for commong emojis'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        //{
        //    id: 'captionsservice',
        //    icon: 'comments-o',
        //    category: 'optimization',
        //    color: '#9bba5c',
        //    name: 'Captions Service',
        //    pages: ['My Videos', 'Upload'],
        //    summary: 'Add professionally transcribed captions to your videos',
        //    howToVideoId: 'BmjoAFiDct4', //'BlOfJCrpdNQ',
        //    Level0Limitation: null,
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: true,
        //    Level1Access: true,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'Increase video views up to 7.3% <a href="http://www.3playmedia.com/customers/case-studies/discovery-digital-networks/" target="_blank">Case Study</a>',
        //        'Captions are transcribed by industry leader - <a href="http://www.3playmedia.com/" target="_blank">3PlayMedia</a>',
        //        'Boost Video SEO, make your video available to a wider audience and let them watch it in more places'],
        //    whereToFindIt: 'Video drop-down menu on My Videos page or "Captions Service" button on Upload page in Post Processing section<br><br><img src="/assets/images/tools/screenshots/captionsservicewhere.png" />',
        //    tipsAndTricks: [
        //        'Captions are priced at $2.25 per minute'
        //    ],
        //    limits: '<div class="limitHeader">Starter | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">$2.25/minute</div>'
        //},
        {
            id: 'tagsorter',
            icon: 'reorder',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Tag Sorter',
            pages: ['Edit Video'],
            summary: 'Reorder your video Tags on any video in any order quickly and easily',
            howToVideoId: 'qoEh7qwQbNc',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'YouTube removed the ability to reorder your Tags a long time ago',
                'You want your most important Tags ranked first',
                'Helps you rank higher in YouTube search results'],
            whereToFindIt: 'Below your list of Tags on a Video Edit screen<br><img src="/assets/images/tools/screenshots/tagsortwhere.png" />',
            tipsAndTricks: [
                'Put your most relevant Tags first.',
                'Move tags that people are searching for to the top of your list to help rank higher in search.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro | Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'tagexplorer',
            icon: 'tags',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Keyword Explorer',
            pages: ['General Page', 'Edit Page', 'Watch Page'],
            summary: 'The ultimate Keyword research tool for YouTube and Google Trends',
            howToVideoId: '2dMFavj55cM',
            Level0Limitation: 'Can only see top 3 results in each category, 25 searches per day.',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Get your videos ranked higher in search results and ultimately get more views',
                'Find long-tail search terms to better target what people are looking for on YouTube ',
                'Discover trending Tags to keep your videos relevant long after they are published'],
            whereToFindIt: '<span style="font-size:12px; font-weight:bold;">[LOCATION 1]</span><br> Click Keyword Explorer from the main TubeBuddy Menu <br /><img src="/assets/images/tools/tagexplorerwhere1.png" /><br><Br><span style="font-size:12px; font-weight:bold;">[LOCATION 2]</span><br> On a Video details screen, click the \'Explore\' button or double-click on any Tag in the Tag list<br/><img src="/assets/images/tools/tagexplorerwhere2.png" /><br/><br><span style="font-size:12px; font-weight:bold;">[LOCATION 3]</span><br> Click on any Tag in the Tag list on a video watch page<br> <img src="/assets/images/tools/tagexplorerwhere3.png" /><br><Br><span style="font-size:12px; font-weight:bold;">[LOCATION 4]</span><br> While you are uploading a new video, click the \'Keyword Explorer\' button<br/><img src="/assets/images/tools/tagexplorerwhere4.png" /><br/><br><span style="font-size:12px; font-weight:bold;">[LOCATION 5]</span><br>From a YouTube Search Results page after clicking the \'View Details\' button, click the magnifying glass next to any tag.<br/><img src="/assets/images/tools/tagexplorerwhere5.png" />',
            tipsAndTricks: [
                'Find Tags that have a high keyword score meaning they are searched often but there is not too much competition.',
                'Unless you are PewDiePie, it\'s hard to get your videos ranked high for broad search terms. Try targeting long-tail (more specific) keywords.',
                'If using Tag Explorer on a Video Edit screen or the Upload screen, you can click the (+) Plus Sign next to any Tag to add it to the Tag List in the video you are editing.',
                'Click on any Tag in the results to re-run tag analysis on that Tag.',
                'The Trending Tab only provides results for more general search terms or very popular specific terms.'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot view more than the top three results in each section</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'tagrankings',
            icon: 'tags',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Tag Rankings',
            pages: ['General Page', 'Edit Page', 'Watch Page'],
            summary: 'Instantly see where you video ranks in search results for each its tags',
            howToVideoId: 'NBMvr_V-rts',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Uses "Incognito" results for more accurate rank positions',
                'Adjust video tags to focus on what you rank for to increase your position',
                'The lower the number the better (1 means you are ranked 1st on the search results page)'],
            whereToFindIt: 'Video Edit Screen',
            tipsAndTricks: [
                'Keep the Incognito Results option turned in for the most accurate results',                
                'Search results vary from person to person, computer to computer so keep in mind that the numbers you see might be slightly different than what someone else sees.'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot view more than the top three results in each section</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'publishtofacebook',
            icon: 'facebook',
            category: 'promotion',
            color: '#445569',
            name: 'Publish to Facebook',
            pages: ['My Videos', 'Upload'],
            summary: 'Publish your YouTube videos natively to Facebook with just a couple clicks',
            howToVideoId: 'DJAaoh74iWQ',
            Level0Limitation: '',
            Level1Limitation: '',
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Facebook is becoming a leader in online video',
                'Make it easier to people to find your content',
                'Monetize your content across multiple social networks'],
            whereToFindIt: 'Upload Page and My Videos page<br><img src="/assets/images/tools/screenshots/publishtofacebookwhere.png" />',
            tipsAndTricks: [
                'Use this tools for ALL your videos!'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">No Access</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'besttimetopublish',
            icon: 'clock',
            category: 'promotion',
            color: '#445569',
            name: 'Best Time to Publish',
            pages: ['My Videos', 'Upload'],
            summary: 'Publish Videos when your Audience is Most Active',
            howToVideoId: '',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Help drive initial view velocity on uploads',
                'Quickly identify when your audience is most active',
                'See when the best time to live stream is'],
            whereToFindIt: 'TubeBuddy Main Menu or Channel Analytics > Audience Tags. ',
            tipsAndTricks: [
                'These are recommendations - the most important thing is consistancy.'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">No Access</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },{
            id: 'channelytics',
            icon: 'area-chart',
            category: 'research',
            color: '#f29b27',
            name: 'Channelytics',
            pages: ['Channel Page'],
            summary: 'Stats & Analysis on any channel in YouTube from StatFire',
            howToVideoId: 'zb11V1jsq6I',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to compare your channel with a competitor\'s channel',
                'You like graphs and data'],
            whereToFindIt: 'Go to any channel page and click the Channelytics Tab',
            tipsAndTricks: [
                'The You vs Them graph is powered by <a target="_blank" href="http://www.statfire.com">StatFire.com</a>.',
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'languageanalysis',
            icon: 'language',
            category: 'research',
            color: '#f29b27',
            name: 'Language Analysis',
            pages: ['Edit Video'],
            summary: 'Get a breakdown of languages spoken by your channel\'s audience',
            howToVideoId: '',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Use the data to choose which languages to translate key Tags to (using our <a href="/tools?tool=tagtranslator">Tag Translator</a>)',
                'Use the data to choose which languages to create Title and Description translations for',
                'Use the data to learn more about your audience and who to target in your videos'],
            whereToFindIt: 'On the <a href="/tools?tool=tagtranslator">Tag Translator</a> tool as well as the Translations Tab on any video Info &amp; Settings screen. <br /><img src="/assets/images/tools/screenshots/languageanalysiswhere.png" />',
            tipsAndTricks: [
                'When using this data for Tags, try to focus on languages with at least 10%'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'retentionanalyzer',
            icon: 'clock-o',
            category: 'research',
            color: '#f29b27',
            name: 'Retention Analyzer',
            pages: [],
            summary: 'See how videos across your channel are performing at various time intervals.',
            howToVideoId: 'QJ51UFuGj04',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Identify what intros or opening sequences are causing viewers to leave.',
                'Work to improve overall retention on your videos by identifying trends in audience viewership.',
                'Test various End Screens to figure out which one performs the best. '],
            whereToFindIt: 'TubeBuddy website channel features',
            tipsAndTricks: [
                'Use the chart as a scoring system'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'thumbnailgenerator',
            icon: 'photo',
            category: 'productivity',
            color: '#445569',
            name: 'Thumbnail Generator',
            pages: ['My Videos', 'Video Upload'],
            summary: 'Create professional quality thumbnails using still frames from your video and text/image overlays',
            howToVideoId: 'oVnILqIhHk8',
            Level0Limitation: 'Can only add one custom image, cannot create templates, cannot download a copy of image',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You spend too much time using Photoshop, taking screenshots, copying, pasting, adding, modifying, saving and uploading',
                'You can save and re-use Templates for a clean, consistent look across your video thumbnails',
                'You are paying for an Adobe Photoshop license just to create YouTube video thumbnails'
            ],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Thumbnail Generator" from the TubeBuddy menu next to the video that you want to generate a thumbnail for.<br><img src="/assets/images/tools/screenshots/thumbnailgeneratorwhere.png" />',
            tipsAndTricks: [
                'Visual Guidelines (from Creator Playbook): Close-ups on faces, well framed, good composition, looks great at both small and large sizes, accurately represents your content and visually compelling imagery',
                'Create and use Templates to save time and get a consistent look across your videos.',
                'Upload your own custom fonts to ensure thumbnails created in TubeBuddy match your existing content',
                'Upload your logo and other brand images to reuse across videos.',
                'Click on any layer (Text or Image) in the preview area to change its properties, rotate, resize or move it.',
                'Your thumbnail will be updated immediately however, it can take a couple hours for the updated thumbnail to show up throughout all of YouTube.'],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody"><ul class="margin-left30"><li>Cannot use Templates</li><li>Cannot upload more than one custom image</li><li>Cannot upload custom fonts</li><li>Cannot download a copy of image</li></ul></div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'coppacenter',
            icon: 'child',
            category: 'productivity',
            color: '#445569',
            name: 'Coppa Center',
            pages: ['My Videos'],
            summary: 'Learn more about COPPA and quickly identify which videos of yours rated "Made for Kids" by  YouTube ',
            howToVideoId: 'L-M8Yvwmy7k',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Learn what COPPA means and how it can affect your channel',
                'Scan your videos to identify which ones are marked as Made for Kids',
                'See which videos that were rated for kids but you overrode as not made for kids'
            ],
            whereToFindIt: 'Click the Coppa Center link under Misc Tools on the Videos List page',
            tipsAndTricks: [
                'Run periodically to ensure you are not missing new ratings from YouTube'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody"><ul class="margin-left30"><li>Cannot use Templates</li><li>Cannot upload more than one custom image</li><li>Cannot upload custom fonts</li><li>Cannot download a copy of image</li></ul></div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'defaultuploadprofiles',
            icon: 'upload',
            category: 'productivity',
            color: '#445569',
            name: 'Default Upload Profiles',
            pages: ['Upload Defaults'],
            summary: 'Create multiple Default Upload Profiles for each type of video you make',
            howToVideoId: 'oYfpUrkn77Q',
            Level0Limitation: 'Can only create one additional upload profile',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'YouTube only allows you to create one set of Upload Defaults',
                'You have a different set of Tags and boilerplate for your description based on what type of video you are uploading',
                'You can apply these settings to existing videos too (on Video Edit screen)'],
            whereToFindIt: '<b>Studio Beta:</b> on YouTube Settings, beside the Upload Defaults tab .<br /><img src="/assets/images/tools/screenshots/upload-defaults-where.png" /><br /><br /><b>Classic Studio: </b>On the <a class="underline" target="_blank" href="https://www.youtube.com/upload_defaults">Default Uploads</a> page on YouTube in the upper-right hand corner.<br /><img src="/assets/images/tools/uploaddefaults.png" />',
            tipsAndTricks: [
                'You can apply your new upload profiles on the Upload page while your video is being uploaded.',
                'Everyone has a different set of options based on their account permissions so if you ever hit a problem, let us know.',
                'Apply your upload profiles to any of your old videos (not just new Uploads).'],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot create more than one set of additional Upload Defaults</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }
        , {
            id: 'cardtemplates',
            icon: 'th-large',
            category: 'productivity',
            color: '#445569',
            name: 'Card Templates',
            pages: ['Video Upload', 'Cards'],
            summary: 'Set any video as a <i>Card Template</i> then easily apply its Cards to new uploads',
            howToVideoId: 'ZQ5hm0hCDIk',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You are manually creating the same set of Cards on all new uploads',
                'You want to handle all aspects of new uploads quickly and efficiently',
                'You have different sets of Cards to apply depending on what type of video you upload'],
            whereToFindIt: 'Set a video as a Cards Template at the bottom of the Cards page for that video (then you will be able to apply this template on the Upload screen after any new video has been uploaded and processed).<br /><img src="/assets/images/tools/screenshots/cardtemplateswhere2.png" /><br /><br /> <b>Studio Beta:</b> Above on the Video Editor page. <br /><img src="/assets/images/tools/screenshots/cardtemplateswhere.png" />',
            tipsAndTricks: [
                'Set up different Card Templates for the different types of videos you create. Then on the Upload screen, choose a template based on the type of video being uploaded.',
                'TubeBuddy uses "Smart Timing" to ensure the Cards get applied in the correct location on your new upload no matter what the duration of the video is.',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'searchranking',
            icon: 'line-chart',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Search Positions',
            pages: ['My Videos', 'Watch Video'],
            summary: 'See where your video ranks for all of its Tags in YouTube search',
            howToVideoId: '7gOmWGCA2_c',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to know where your video ranks in YouTube search',
                'You want to identify Tags that aren\'t helping you rank and should therefore be removed',
                'Coming soon: You want to see you video rank position changes week to week'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Search Rankings" from the TubeBuddy menu next to the video that you want to see its search rankings.<br /><img src="/assets/images/tools/screenshots/searchrankingswhere.png"/>',
            tipsAndTricks: [
                'Add your own keywords to track beyond just your Tags',
            ],
            limits: '<div class="limitHeader">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited (Subject to change)</div>'
        }, {
            id: 'sharetracker',
            icon: 'twitter',
            category: 'promotion',
            color: '#bd382f',
            name: 'Share Tracker',
            pages: ['My Videos', 'Watch Video', 'Video Upload'],
            summary: 'Share your video on multiple social networks and track which ones you\'ve shared to',
            howToVideoId: '7pNGyDzaaAM',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want your video to spread across all social networks',
                'You always forget which social networks you have shared your videos to',
                'You don\'t want to have to go to the video watch page just to share your video on other social networks'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Share Tracker" from the TubeBuddy menu next to the video you want to share and track.<br /><img src="/assets/images/tools/screenshots/sharetrackerwhere.png" />',
            tipsAndTricks: [
                'You can click on the green/grey dot to toggle the status as sent/not sent.',
                'We hope to some day automate the sharing piece rather than having you manually click to share.'],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'vid2vid',
            icon: 'file-text-o',
            category: 'promotion',
            color: '#bd382f',
            name: 'Vid2Vid Promotion',
            pages: ['My Videos', 'Video Upload'],
            summary: 'Promote one of your videos in the descriptions of all your other videos',
            howToVideoId: 'y9p_f5PLkhg',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to easily drive viewers of your old videos to your newest upload',
                'You want to showcase an older video that is relevant to trending topics',
                'You want to feature a video that is running a contest'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Promote this Video (Vid2Vid)" from the TubeBuddy menu next to the video that you want to promote.<br /><img src="/assets/images/tools/screenshots/vid2vidwhere.png" />',
            tipsAndTricks: [
                'Each time you run this tool, it <b>replaces</b> your previous promotion (rather than just appending it to your description)',
                'You can customize the wording - make sure to choose something that a viewer is likely to click',
                'As of right now, you can\'t easily remove a Description Promotion. You can only replace it. To remove it, you would have to use the Bulk Find / Replace / Append tools and remove each line seprately.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'advancedvideoembedding',
            icon: 'code',
            category: 'productivity',
            color: '#445569',
            name: 'Advanced Video Embed',
            pages: ['My Videos', 'Watch Video', 'Playlist Edit'],
            summary: 'Create embed codes with the full list of options for adding videos or playlists to your website',
            howToVideoId: 'dFUYsbbf6U0',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'YouTube only gives you a few basic options for embedding videos and playlists',
                'You want to customize the look of your embedded video or playlist',
                'You don\'t want to have to go to the video watch page to get the embed code for your own video'],
            whereToFindIt: 'Drop down next to any video on your <a target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page, "Tools" tab (above Description) on any <a target="_blank" href="https://www.youtube.com/watch?v=oJddzb2DKTU">Video Watch Page</a> or any edit Playlist page<br /><img alt="advanced embed image" src="/assets/images/tools/screenshots/advanceembedwhere.png" />',
            tipsAndTricks: [
                'Click on on any of the options to learn more about what it does.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'quicklinksmenu',
            icon: 'link',
            category: 'productivity',
            color: '#445569',
            name: 'Quick Links Menu',
            pages: ['General Page'],
            summary: 'Easily navigate to common areas on YouTube without having to click 17 times',
            howToVideoId: 'oJcelF4Ao90',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'YouTube makes you click 47 times and load 13 pages to simply get to your My Videos page (note: we maybe have exaggerated slightly)',
                'There are only a few areas that you go to all the time and you should be able to get to them more easily',
                'Coming soon: Customize the Quick Menu with your own links'],
            whereToFindIt: 'Click the main TubeBuddy menu next to the Upload button on YouTube.<br /><img src="/assets/images/tools/screenshots/quicklinksmenuwhere.png" />',
            tipsAndTricks: [
                'We hope to allow you to customize this menu in the near future',
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'quickedittoolbar',
            icon: 'pencil',
            category: 'productivity',
            color: '#445569',
            name: 'Quick-Edit Toolbar',
            pages: ['General Page', 'Video Edit', 'Edit Video', 'Annotations', 'Cards', 'My Videos', 'Playlist', 'All Playlists'],
            summary: 'Easily navigate between videos when editing a list',
            howToVideoId: 'wo6KA2-Q55g',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'There is no way to easily edit videos in a Playlist on YouTube without it',
                'You are tired of having to click the My Videos page every time you want to edit another video',
                'You like to be able to easily jump between videos'],
            whereToFindIt: 'Any Video Edit screen - assuming you came from a Playlist page or My Videos page',
            tipsAndTricks: [
                'Go to your <A href="https://www.youtube.com/view_all_playlists" target="_blank">Playlists</a> page to easily edit any playlist',
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        //{
        //    id: 'subscriberoutreach',
        //    icon: 'bullhorn',
        //    category: 'promotion',
        //    color: '#bd382f',
        //    name: 'Subscriber Outreach',
        //    pages: ['Subscribers'],
        //    summary: 'Reach out to your YouTube Subscribers across multiple social networks',
        //    howToVideoId: '',
        //    Level0Limitation: null,
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: true,
        //    Level1Access: true,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'You want to build relationships with your audience',
        //        'You want to collaborate with fans of your channel',
        //        'You see what your popular subscribers are doing on social media'],
        //    whereToFindIt: 'Next to each subscriber in your Subscribers list',
        //    tipsAndTricks: [
        //        'Thank your audience for subscribing every day',
        //        'DO NOT SPAM.',
        //        'Say hello to your subscribers on other networks outside of YouTube'],
        //    limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot use Community Connect on videos that aren\'t yours</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        //},
        //{
        //    id: 'commentfilters',
        //    icon: 'filter',
        //    category: 'productivity',
        //    color: '#445569',
        //    name: 'Comment Filters',
        //    pages: ['Comments'],
        //    summary: 'Easily manage large amounts of comments using our advanced filtering options',
        //    howToVideoId: '0dab17xEusc',
        //    Level0Limitation: 'Cannot Hide comments, can only use on the "All Videos" section of comments',
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: false,
        //    Level1Access: true,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'You\'re having trouble keeping up with comments on your channel because you get so many',
        //        'YouTube\'s comment managing system is clunky and hard to use',
        //        'You want to quickly and easily find questions, profanity, positive sentiment, negative sentiment or certain words within comments on your videos'],
        //    whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/comments">Comments</a> page on YouTube and look right above the first comment.<br /><img src="/assets/images/tools/commentfilterswhere.png" />',
        //    tipsAndTricks: [
        //        'Use this tool to find people who have asked questions on your videos but you haven\'t responded.',
        //        'Want new filters added to this tool? Let us know!'],
        //    limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot use Comment Filters on individual videos</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        //},
        //{
        //    id: 'realtimesubscribercounts',
        //    icon: 'users',
        //    category: 'research',
        //    color: '#f29b27',
        //    name: 'Real-Time Sub Counts',
        //    pages: ['Channel Page'],
        //    summary: 'See real-time subscriber counts for any channel',
        //    howToVideoId: 'zb11V1jsq6I',
        //    Level0Limitation: null,
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: true,
        //    Level1Access: true,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'You want to see how fast you are gaining subscribers',
        //        'You\'re curious how fast the big guys (i.e. PewDiePie) gain subscribers',
        //        'You want to keep tabs on your competitiion'],
        //    whereToFindIt: 'Go to any channel page and click the Channelytics Tab<br /><img src="/assets/images/tools/channelyticswhere.png" />',
        //    tipsAndTricks: [
        //        'Run the backup process often to ensure you never lose your hard earned work',
        //        'Someday we hope to allow backing up of video files themselves',
        //        'Someday we hope to add a way to restore a channel from CSV'],
        //    limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">Cannot export data to CSV</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        //},
        {
            id: 'channelbackup',
            icon: 'download',
            category: 'research',
            color: '#f29b27',
            name: 'Channel Backup',
            pages: ['My Videos'],
            summary: 'Protect yourself against lost data by backing up all your videos\' metadata',
            howToVideoId: '0k7bEQNIyHY',
            Level0Limitation: null,
            Level1Limitation: 'Cannot export results to CSV',
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want peace of mind knowing that your channel information is backed up',
                'You want to play with video metadata in Excel or Google Docs',
                'You want to search, filter or sort video data using a spreadsheet application'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and click the "Backup" button.<br /><img src="/assets/images/tools/screenshots/channelbackupwhere.png" />',
            tipsAndTricks: [
                'Run the backup process often to ensure you never lose your hard earned work',
                'Someday we hope to allow backing up of video files themselves',
                'Someday we hope to add a way to restore a channel from CSV'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">Cannot export data to CSV</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'milestones',
            icon: 'trophy',
            category: 'promotion',
            color: '#f29b27',
            name: 'Milestones',
            pages: ['My Videos'],
            summary: 'Celebrate your channel\'s successes',
            howToVideoId: 'dpFtudhGtuM',
            Level0Limitation: 'Can not select custom backgrounds',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to share your recent achievements on social media',
                'You want to print and frame a special channel milestones',
                'You want to surprise a friend by showing off their channel\'s milestone'],
            whereToFindIt: 'From the TubeBuddy main menu on YouTube, or, go to <a target="_blank" href="https://www.tubebuddy.com/milestones">TubeBuddy.com/Milestones</a>.',
            tipsAndTricks: [
                'Don\'t just celebrate your own milestones, celebrate everyones!',
                'Celebrate milestones but do not get caught up in the numbers. Focus on great content and the achievements will come.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">Cannot export data to CSV</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'exportcomments',
            icon: 'file-excel-o',
            category: 'research',
            color: '#f29b27',
            name: 'Export Comments',
            pages: ['My Videos'],
            summary: 'Export Video Comments to CSV File',
            howToVideoId: 'yeT8blc5rPs',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to search comments for particular words',
                'You want to run analysis on your comment pages',
                'You want to identify top influencers that are active on your channel'],
            whereToFindIt: 'Video Tools menu on any video on the <a href="https://www.youtube.com/my_videos" target="_blank">My Videos</a> page',
            tipsAndTricks: [
                ''],
            limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">Cannot export data to CSV</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'cannedresponses',
            icon: 'file-text-o',
            category: 'productivity',
            color: '#445569',
            name: 'Canned Responses',
            pages: ['Subscribers', 'Watch Video'],
            summary: 'Use pre-written messages in various areas of YouTube ',
            howToVideoId: 'Nnrl_BhFrnA',
            Level0Limitation: 'Can only create 1 Canned Response',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You find yourself writing the same response over and over in YouTube',
                'You want to personalize messages but write them faster',
                'You want to write messages in your own wording, then have someone else use TubeBuddy to actually send the responses'],
            whereToFindIt: 'Click on the main TubeBuddy menu next to the Upload button then under Website Tools select Canned Responses.<br /><img src="/assets/images/tools/cannedresponses.png" />',
            tipsAndTricks: [
                'Use {N} as a placeholder which will insert the channel\'s username into the message and personalize it.',
                'Currently available in Subscribers List page and Community Connect tool. Want it in other areas? Let us know!'],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot create more than one Canned Response</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'pickawinner',
            icon: 'trophy',
            category: 'promotion',
            color: '#bd382f',
            name: 'Pick a Winner',
            pages: ['My Videos', 'Watch Video'],
            summary: 'Randomly select a user who has commented on one of your videos',
            howToVideoId: 'ZIxUlz96c8s',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You are running a contest and need to randomly pick a winner',
                'You want access to social profiles of a random commenter on a video',
                'You want to flip through random comments on a video'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Pick a Winner" from the TubeBuddy menu next to the video that you want to want to randomly select a commenter from.<br /><img src="/assets/images/tools/screenshots/pickawinnerwhere.png" />',
            tipsAndTricks: [
                'If you are selecting more than one winner, simply click the button again'],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'promomaterials',
            icon: 'bullhorn',
            category: 'promotion',
            color: '#bd382f',
            name: 'Promotion Materials',
            pages: [''],
            summary: 'Specialized links and code for promoting your channel and videos',
            howToVideoId: '40AbO-orgbQ',
            Level0Limitation: 'Cannot create custom links or run on custom video Ids',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to boost new uploads by linking to them in various areas',
                'You want to add your most recent or most popular upload to an email signature',
                'You want to embed a video player on your blog or website that always plays your most recent video'],
            whereToFindIt: 'Log in at TubeBuddy.com/account and click "Promo Materials" under a channel.',
            tipsAndTricks: [
                ''],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'channelvaluation',
            icon: 'dollar',
            category: 'research',
            color: '#9bba5c',
            name: 'Channel Valuation',
            pages: ['Dashboard'],
            summary: 'Know your Worth. Brand Deal Valuations via SocialBluebook.com',
            howToVideoId: 'R5x4qN99WVQ',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to know how much to charge brands for a dedicated video',
                'You want to know how much to charge brands for a shoutout',
                'You want to monitor your channel\'s worth and see if it is rising or falling'],
            whereToFindIt: '<a href="https://www.youtube.com/dashboard">YouTube Dashboard</a>.',
            tipsAndTricks: [
                'If you do not see your valuation, head to <a href="https://socialbluebook.idevaffiliate.com/111.html">socialbluebook.com</a> and link your channel',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot re-run analysis after the first time</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'channelaccess',
            icon: 'lock',
            category: 'research',
            color: '#9bba5c',
            name: 'Channel Access',
            pages: [],
            summary: 'Grant specific people access to your TubeBuddy account',
            howToVideoId: 'bJmJIhd4JW4',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to give another Creator access to your channel Health Report',
                'You want to give your client access to view your progress and their statistics',
                'You want to share your TubeBuddy license among employees in your company'],
            whereToFindIt: 'In the Settings area of a channel on <a href="https://www.tubebuddy.com/account" target="_blank">TubeBuddy.com/Account',
            tipsAndTricks: [
                '',
                '',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot re-run analysis after the first time</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'bulkupdatecards',
            icon: 'refresh',
            category: 'bulk',
            color: '#445569',
            name: 'Bulk Update Cards',
            pages: ['My Videos'],
            summary: 'Automate the process of updating Cards across some or all of your videos',
            howToVideoId: 'eI6FEimLNsg',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Manually updating Cards on all your videos one by one takes forever and is a pain in the &#^@@$',
                'You want to replace an old promotion link on all Cards to a new promotion link',
                'You want to test different Teaser Text or Call To Action to see which converts better'
            ],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Update" under "Bulk Card Updates" in the TubeBuddy menu at the top of the page. <br /><img src="/assets/images/tools/screenshots/bulkupdatecardswhere.png" />',
            tipsAndTricks: [
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot update Cards to more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'videoabtests',
            icon: 'adjust',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Video A/B Tests',
            pages: [],
            summary: 'Test Titles, Thumbnails, Tags and Descriptions to find what works best',
            howToVideoId: 'og_RxIVaDNs',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level3Limitation: 'Max of 10 concurrent tests',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: false,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to back up theories about what thumbnails work best for your audience',
                'You want to find the best style for your Titles to drive the most clicks',
                'You want to see if changes to your Title, Description or Tags affects your search traffic'
            ],
            whereToFindIt: 'TubeBuddy.com/Account (then click AB Tests under your channel)<br><br>Want to use A/B Tests to improve your YouTube SEO?<br/>Check out <a href="https://www.milesbeckler.com/youtube-seo/#videoseosoftware">https://www.milesbeckler.com/youtube-seo/#videoseosoftware</a>',
            tipsAndTricks: [
                '(coming soon!)'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'competitorscorecard',
            icon: 'pie-chart',
            category: 'research',
            color: '#f29b27',
            name: 'Competitor Scorecard',
            pages: [],
            summary: 'See how your channel stacks up against the competition',
            howToVideoId: 'NUoGzwGzzW8',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level3Limitation: 'Max 10 Competitors',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: false,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to see how you compare with the competition in views, subscribers, uploads and engagement',
                'You want a printable report showing data about you vs your competition',
                'You want to Export to CSV YouTube data about your competition'
            ],
            whereToFindIt: 'TubeBuddy.com (click the "Competitor Scorecard" link on the main TubeBuddy menu on YouTube.com)',
            tipsAndTricks: [
                'Has Print button to get a printer friendly copy of the Scorecard',
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star </div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'competitoruploadalerts',
            icon: 'bell',
            category: 'research',
            color: '#f29b27',
            name: 'Competitor Upload Alerts',
            pages: [],
            summary: 'Get notification when a competitor uploads a video',
            howToVideoId: 'ucWK7MyF3IA',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level3Limitation: 'Max 10 Competitors',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: false,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to be notified when a competitor uploads a video.',
                'You can choose to be notified via email, TubeBuddy notification or text.'
            ],
            whereToFindIt: 'Competitor icon on any of your linked channels at <a href="https://www.tubebuddy.com/account" target="_blank">TubeBuddy.com/account</a> and then click on Manage',
            tipsAndTricks: [
                'Use this tool to keep track of your competitions video uploads.',
                'This tool will notify you within minutes of an upload so you can monitor competitor activities.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star </div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'brandalerts',
            icon: 'bell',
            category: 'research',
            color: '#f29b27',
            name: 'Brand Alerts',
            pages: [],
            summary: 'Monitor YouTube for new videos, playlists and channels uploaded about you or your brand',
            howToVideoId: 'CTctBDrbtDY',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: 'Limited to just 1 Brand Alert',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to know when someone uploads a review video about your product',
                'You want to track uploads about your competitors',
                'You want to monitor general sentiment about you or your brand on YouTube'
            ],
            whereToFindIt: 'Brand Alert icon on any of your linked channels at <a href="https://www.tubebuddy.com/account" target="_blank">TubeBuddy.com/account</a>. <img src="/assets/images/tools/screenshots/brandalertswhere.png" />',
            tipsAndTricks: [
                'Use this tool to thank your fans for uploading content about you!',
                'Use this tool to track and find illegal uploaded content'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Star | Network</div><div class="limitbody">You can create 1 Brand Alert</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'healthreport',
            icon: 'heartbeat',
            category: 'research',
            color: '#f29b27',
            name: 'Health Report',
            pages: [],
            summary: 'Get an overall look at the health and performance of your channel',
            howToVideoId: 'ZybNWMQQdA8',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to discover what\'s working and what isn\'t on your channel',
                'You want quick access to analytics - demographics, search traffic, related videos, watch time, etc',
                'You want to improve your channel based on data insights'
            ],
            whereToFindIt: 'TubeBuddy.com Website (click heartbeat icon next to channel)',
            tipsAndTricks: [
                'Data is updated once per day when YouTube Analytics are refreshed',
                'Use information to find out what is working and what is not on your channel and make adjustments'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Pro | Star | Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'commentcloud',
            icon: 'cloud',
            category: 'research',
            color: '#f29b27',
            name: 'Comment Word Cloud',
            pages: [],
            summary: 'Get a visual representation of what people are saying about your videos',
            howToVideoId: 'jIiBOSvDM0M',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Understand if people have a positive or negative sentiment about a video',
                'Get new video ideas by seeing what people are talking most about in your videos',
                'Get a quick overall sense for how a video performed and what people think about it'
            ],
            whereToFindIt: 'Video Watch pages </br></br><img src="/assets/images/tools/screenshots/wordcloud-where2.jpg" />',
            tipsAndTricks: [
                'Click the button again after loading more comments to refresh the cloud',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Pro | Star | Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'clickmagnet',
            icon: 'magnet',
            category: 'research',
            color: '#f29b27',
            name: 'Click Magnet',
            pages: [],
            summary: 'Advanced analytics to help win the click and get viewers watching.',
            howToVideoId: 'CruHf8SMY4c',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Determine which videos perform best for your channel',
                'Discover which elements of your thumbnails and titles are driving the highest CTR',
                'Identify high performing videos where slight tweaks can make the biggest impact',
                'Group, sort, view and export your most important video performance data'
            ],
            whereToFindIt: 'On the Channel Videos Page, Click Bulk & Misc Tools <img src="/assets/images/tools/screenshots/CM Where.png" />',
            tipsAndTricks: [
                'Videos are ranked relative to the videos on your channel, not any other channel’s videos',
                'You may see a common thread of the types of content that performs, or doesn’t perform, for your channel, and you should adjust your content strategy accordingly',
                'Click on the bars in the Element Inspector graphs to see all of the thumbnails in that category',
                'Export the data from the Advanced Analytics tab to build your own reports and analysis'
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Pro | Star | Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'autotranslator',
            icon: 'language',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Tag Translator',
            pages: ['Edit Video'],
            summary: 'Translate video Tags into other languages to increase global viewership',
            howToVideoId: '25OHhrk5QyU',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level3Limitation: 'Max 50 translations per day',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Your want people all around the world find and watch your video',
                'You want your video showing up as a Related Video on other videos around the world',
                'You recognize that YouTube is a global community and want to connect with everyone'],
            whereToFindIt: 'Below your list of Tags on a Video Edit screen and on the Translations Tab on the video edit page <br><img src="/assets/images/tools/screenshots/autotranslatorwhere.png" />',
            tipsAndTricks: [
                'You are limited in the total number of Tags on a video so only translate your most relevant Tags.',
                'Check out your Analytics before deciding which Tags to translate and see what countries you are getting views from.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">No access</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'metaautotranslator',
            icon: 'language',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Auto Translator',
            pages: ['Edit Video, Upload Page'],
            summary: 'Translate video Title and Description into other languages to increase global viewership',
            howToVideoId: null,
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level3Limitation: 'Max 50 translations per day',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Your want people all around the world find and read the video title and description in their own language',
                'You want your video showing up as a Related Video on other videos around the world',
                'You recognize that YouTube is a global community and want to connect with everyone'],
            whereToFindIt: 'On a Video Edit screen on the Subtitles tab. <br><img src="/assets/images/tools/screenshots/metaautotranslatorwhere.png" />',
            tipsAndTricks: [
                'You are limited in the total number of title and descriptions per day, 50.',
                'Check out your Analytics before deciding which Tags to translate and see what countries you are getting views from.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">No access</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'videotopicplanner',
            icon: 'edit',
            category: 'productivity',
            color: '#445569',
            name: 'Video Topic Planner',
            pages: ['General Page', 'Edit Page', 'Watch Page'],
            summary: 'Easily manage and get ideas for future video topics',
            howToVideoId: 'BKf_y6zyV5I',
            Level0Limitation: 'Maximum of 5 video topics and comment suggestions',
            Level1Limitation: '',
            Level2Limitation: '',
            Level3Limitation: '',
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You have trouble coming up with ideas for new videos',
                'You keep forgetting those great video suggestions from your audience in the video comments area',
                'You don\'t have an easily accessible and reliable way of saving and tracking future video ideas',
            ],
            whereToFindIt: 'Click Video Topic Planner from the main TubeBuddy Menu<br><img src="/assets/images/tools/screenshots/videotopicplannerwhere.png" />',
            tipsAndTricks: [
                'You can sort your list of topics by dragging them up/down in the list',
                'Head to the comments section of any of your videos, click the TubeBuddy icon next to a comment and then check the box next to "Comment Suggestion" and it will show up in the Video Topic Planner tool',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody"><ul class="margin-left30"><li>Max of 5 Topics</li><li>Max of 5 Comment Suggestions</li></ul></div><div class="margin-top10 limitHeader2"> | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'bestpractice',
            icon: 'book',
            category: 'optimization',
            color: '#445569',
            name: 'Best Practice Audit',
            pages: ['My Videos'],
            summary: 'Automated checks to ensure you\'re following YouTube\'s recommendations',
            howToVideoId: 'JeMrZmqpeSk',
            Level0Limitation: '',
            Level1Limitation: '',
            Level2Limitation: '',
            Level3Limitation: '',
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You you want to ensure your video is set up correctly',
                'You want to make sure there are no broken links in your video descriptions',
                'You want to remember to add specific words or phrases in your titles/tags/descriptions',
            ],
            whereToFindIt: 'My Videos page, video edit page, and the TubeBuddy.com website',
            tipsAndTricks: [
                'Create your own Best Practice items specific to your channel',
                'Disable any Best Practice checks that don\'t apply to your channel',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody"><ul class="margin-left30"><li>Max of 5 Topics</li><li>Max of 5 Comment Suggestions</li></ul></div><div class="margin-top10 limitHeader2"> | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'opportunities',
            icon: 'lightbulb-o',
            category: 'optimization',
            color: '#445569',
            name: 'Opportunity Finder',
            pages: ['My Videos'],
            summary: 'Suggestions for channel growth based on video performance and analytics',
            howToVideoId: 'lkakEuHCY40',
            Level0Limitation: '',
            Level1Limitation: '',
            Level2Limitation: '',
            Level3Limitation: '',
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You are missing out on opportunities outside of YouTube\'s website',
                'It would take you too long to research items that TubeBuddy automatically finds for you',
                'You can potentially gain views, subscribers and more engagement by following our recommendations',
            ],
            whereToFindIt: 'My Videos page and TubeBuddy.com website',
            tipsAndTricks: [
                'You can disable Opportunities that don\'t apply to you',
                'You can automate the Opportunity Finder on the Launch Pad Settings page if you have upgraded access',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody"><ul class="margin-left30"><li>Max of 5 Topics</li><li>Max of 5 Comment Suggestions</li></ul></div><div class="margin-top10 limitHeader2"> | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'bulkcopyendscreen',
            icon: 'copy',
            category: 'bulk',
            color: '#445569',
            pages: ['My Videos'],
            name: 'Bulk Copy End Screen',
            summary: 'Automate the process of copying End Screens across some or all of your videos',
            howToVideoId: '_U4NewnOCNs',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Manually adding End Screen Elements on all your videos one by one takes forever and is a pain in the &#^@@$',
                'Copying End Screen Elements through TubeBuddy ensures you have a clean, consistent look across all your videos',
                '<a href="https://support.google.com/youtube/answer/6388789?p=end_screens&hl=en&rd=1" target="_blank">End Screens</a> are extremely important for marketing your brand on desktop and mobile'],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Bulk Copy End Screen" from the TubeBuddy menu next to the video that you want to copy Cards <i>from</i>.<br /><img src="/assets/images/tools/screenshots/bulkcopyendscreenwhere.png" />',
            tipsAndTricks: [
                'Don\'t worry if your videos have different durations. TubeBuddy uses "Smart Timing" to figure out where to place the End Screen Elements on each destination video.',
                'If want to get rid of all existing End Screen Elements from the destination videos before copying the new Elements, simply select the "Delete all existing End Screen Elements " option in Step 2.',
                'Each video is processed 1 at a time and takes a second or two. So 1,000 videos might take about 30 minutes to process. We recommend limiting bulk copying to 2,000 or less videos. If you have more, consider breaking them up into playlists and running the tool for each playlist separately.',
                'After processing is complete, you can click on the video link in the <i>Completed</i> area to quickly verify that it was done correctly.',
                'You can customize End Screen Element properties on individual destination videos in Step 3.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot copy End Screen Elements to more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'bulkdeleteendscreen',
            icon: 'remove',
            category: 'bulk',
            color: '#445569',
            pages: ['My Videos'],
            name: 'Bulk Delete End Screen',
            summary: 'Automate the process of deleting End Screens Elements across some or all of your videos',
            howToVideoId: null,
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Manually deleting End Screen Elements on all your videos one by one takes forever and is a pain in the &#^@@$',
                'Delete an End Screen Element for a site, video or playlist you no longer want to promote'
            ],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select the Bulk TubeBuddy menu above your list of videos.  Then select the "Delete" sub menu under the "End Screens" menu.<br /><img src="/assets/images/tools/screenshots/bulkdeleteendscreenwhere.png" />',
            tipsAndTricks: [
                'Each video is processed 1 at a time and takes a second or two. So 1,000 videos might take about 30 minutes to process. We recommend limiting bulk copying to 2,000 or less videos. If you have more, consider breaking them up into playlists and running the tool for each playlist separately.',
                'After processing is complete, you can click on the video link in the <i>Completed</i> area to quickly verify that it was done correctly.'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot delete End Screen Elements to more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'endscreentemplates',
            icon: 'th-large',
            category: 'productivity',
            color: '#445569',
            name: 'End Screen Templates',
            pages: ['Video Upload, End Screen, My Videos'],
            summary: 'Set any video as a <i>End Screen Template</i> then easily apply its End Screen Elements to new uploads',
            howToVideoId: 'bzLZu7nO8ZA',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You are manually creating the same set of End Screen Elements on all new uploads',
                'You want to handle all aspects of new uploads quickly and efficiently',
                'You have different sets of End Screen Elements to apply depending on what type of video you upload'],
            whereToFindIt: 'Set a video as a End Screen Template at the bottom of the End Screen page for that video (then you will be able to apply this template on the Upload screen after any new video has been uploaded and processed).<br /><img src="/assets/images/tools/endscreentemplateswhere2.png" /><br /><br /><b>Studio Beta:</b> Above on the Video Editor page. <br /><img src="/assets/images/tools/screenshots/endscreentemplateswhere.png" />',
            tipsAndTricks: [
                'Set up different End Screen Templates for the different types of videos you create. Then on the Upload screen, choose a template based on the type of video being uploaded.',
                'TubeBuddy uses "Smart Timing" to ensure the End Screen get applied in the correct location on your new upload no matter what the duration of the video is.',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'shareontwitter',
            icon: 'twitter',
            category: 'promotion',
            color: '#445569',
            name: 'Share on Twitter',
            pages: ['Video Upload, My Videos'],
            summary: 'Easy share your YouTube videos on Twitter and automatically attach your thumbnail to it',
            howToVideoId: '69_I5MeVImk',
            Level0Limitation: 'TubeBuddy branding on Post',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to easily share your videos on Twitter directly from YouTube',
                'You want to attach your video thumbnail for a more eye-catching Tweet',
                'You want to help drive more draffic and views to your videos'],
            whereToFindIt: 'Any video list or video edit screen. <br /><img src="/assets/images/tools/screenshots/shareontwitterwhere.png" />',
            tipsAndTricks: [
                'You can add as many Twitter accounts as you would like to post from',
                'Check the box to attach your Thumbnail for a more visually appealing post',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'bulkupdateendscreens',
            icon: 'list',
            category: 'bulk',
            color: '#445569',
            name: 'Bulk Update End Screens',
            pages: ['My Videos'],
            summary: 'Automate the process of updating End Screen across some or all of your videos',
            howToVideoId: 'e2baTTR9ExU',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Manually updating End Screens on all your videos one by one takes forever and is a pain in the &#^@@$',
                'You want to replace an old promotion link on all End Screens to a new promotion link',
                'You want to test different Titles or Images to see which converts better'
            ],
            whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://www.youtube.com/my_videos">My Videos</a> page on YouTube and select "Update" under "Bulk End Screen Updates" in the TubeBuddy menu at the top of the page. <br /><img src="/assets/images/tools/screenshots/bulkupdateendscreenswhere.png" />',
            tipsAndTricks: [
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Pro</div><div class="limitbody">Cannot update End Screens to more than one video at a time</div><div class="margin-top10 limitHeader2">Network | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'taglists',
            icon: 'list-alt',
            category: 'optimization',
            color: '#445569',
            name: 'Tag Lists',
            pages: ['General Page', 'Edit Page', 'Watch Page', 'Upload Page'],
            summary: 'Create and manage centralized lists of Tags',
            howToVideoId: 'ToVuDS_i49U',
            Level0Limitation: 'Maximum of 3 lists',
            Level1Limitation: '',
            Level2Limitation: '',
            Level3Limitation: '',
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to store related tags for later use',
                'You want quick access to certain sets of tags',
                'You want to ditch your Excel spreadsheets'
            ],
            whereToFindIt: 'My Videos page<br><img src="/assets/images/tools/screenshots/taglistswhere.png" /><br><br><br>[LOCATION 2] On a video edit screen, click the "Copy Tags" button<img src="/assets/images/tools/screenshots/copytags-where.png" />',
            tipsAndTricks: [

            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody"><ul class="margin-left30"><li>Max of 3 Lists</li><li>Max of 3 Lists</li></ul></div><div class="margin-top10 limitHeader2"> | Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'keywordranktracking',
            icon: 'list-ol',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Search Rank Tracking',
            pages: [],
            summary: 'Track your videos rankings (and your competitors) across YouTube and Google search for desired Keywords',
            howToVideoId: 'sIKtzCzEiv4',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: 'Max 5 Keywords, No Competitor Tracking, YouTube only (no Google)',
            Level3Limitation: 'Max 50 Keywords',
            Level0Access: false,
            Level1Access: false,
            Level2Access: false,
            Level3Access: false,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to track your Video SEO efforts',
                'You want to see where you rank compared to your Competitors for certain keywords',
                'You want and overall status of how your channel is performing in YouTube and Google search'
            ],
            whereToFindIt: 'TubeBuddy Star, Legend or Enterprise Dashboard',
            tipsAndTricks: [
                'Think about what people might search to find your product',
                'Try to out-optimize your competitors'
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star </div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'videolytics',
            icon: 'line-chart',
            category: 'research',
            color: '#9bba5c',
            name: 'Videolytics',
            pages: [],
            summary: 'Access advanced analytics and insights, instantly for any YouTube video',
            howToVideoId: 'TI5bccXdNBk',
            Level0Limitation: 'Can only compare video to channel\'s most popular video',
            Level1Limitation: null,
            Level2Limitation: null,
            Level3Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Research competitor videos to see what\'s working and not working for them',
                'Get insights on trending videos to see what\'s hot',
                'Compare any video on YouTube with another video across a wide variety of stats'
            ],
            whereToFindIt: 'Video Watch Page',
            tipsAndTricks: [
                ''
            ],
            limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star </div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'scheduledvideoupdate',
            icon: 'calendar',
            category: 'productivity',
            color: '#445569',
            name: 'Scheduled Video Update',
            pages: ['My Videos', 'Edit Video', 'Upload'],
            summary: 'Schedule video updates at a future date/time ',
            howToVideoId: 'Tp1F7ERoSVQ',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: false,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to schedule a change to a video\'s Title, Tags, Description or Thumbnail in the future',
                'You want to and or remove a video from playlists at a certain time in the future',
                'You want to schedule a privacy change (Public / Unlisted / Private) to your video in the future'],
            whereToFindIt: 'On the My Videos page and Edit Video page. <br /><img src="/assets/images/tools/screenshots/scheduledvideoupdatewhere.png" />',
            tipsAndTricks: [
                'If you want to run a promotion, use the Rollback feature to automatically roll-back your changes at a later date/time.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro </div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Star | Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'searchexplorer',
            icon: 'search',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Search Explorer',
            pages: ['Search Results'],
            summary: 'The ultimate Search Term research tool for YouTube',
            howToVideoId: '6F5HL6MVp5k',
            Level0Limitation: 'Can only see top 3 results in each category, 25 searches per day. ',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'Get your videos ranked higher in search results and ultimately get more views',
                'Find long-tail search terms to better target what people are looking for on YouTube ',
                'Discover search terms to target based on search volume, competition and overall score'],
            whereToFindIt: 'On the Search page. *Only available on YouTube\'s new UI  <br /><img src="/assets/images/tools/screenshots/searchexplorerwhere.png" />',
            tipsAndTricks: [
                'Find Tags that have a high keyword score meaning they are searched often but there is not too much competition.',
                'Unless you are PewDiePie, it\'s hard to get your videos ranked high for broad search terms. Try targeting long-tail (more specific) keywords.',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot view more than the top three results in each section</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        }, {
            id: 'searchinsights',
            icon: 'lightbulb-o',
            category: 'research',
            color: '#9bba5c',
            name: 'Search Insights',
            pages: ['Search Results'],
            summary: 'Uncover information about videos and channels in the search results',
            howToVideoId: null,
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'YouTube’s search results are built for the viewer in mind, not for a creator who’s doing research',
                'Gain insights into tags, channel subscriber counts, and likes/dislikes for videos that are ranking for a given search term - all within the search results page'],
            whereToFindIt: 'On the YouTube Search Results page.  <br /><img src="/assets/images/tools/screenshots/searchinsightswhere.png" />',
            tipsAndTricks: [
                'Click the View Details button at the top of the search results page to display tags, channel subscriber counts, and likes/dislikes for video search results.',
                'Click the magnifying glass icon for a tag to research it further in Keyword Explorer.',
                'Click the Copy Tags button to copy a video’s tags to a Tag List or your clipboard.'
            ],
            limits: ''
        }
        , {
            id: 'instasuggest',
            icon: 'magic',
            category: 'optimization',
            color: '#9bba5c',
            name: 'Insta-Suggest',
            pages: ['Edit Video', 'Upload'],
            summary: 'See tag suggestions in real-time as you type',
            howToVideoId: 'dQ3zTDZXYoQ',
            Level0Limitation: 'Can only see top 3 results',
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to add tags matching what people are searching for',
                'You are <span style="text-decoration: line-through;">lazy</span> efficient and want to see results as you type',
                'You want helpful hints on popular trends and search phrases'],
            whereToFindIt: '<img src="/assets/images/tools/screenshots/instasuggestwhere.png" />',
            tipsAndTricks: [
                'Click the Explore button at the bottom to dive deeper into the topic',
                'Click the Suggest button at the bottom for more suggestions for the video',
            ],
            limits: '<div class="limitHeader">Starter (FREE)</div><div class="limitbody">Cannot view more than the three results</div><div class="margin-top10 limitHeader2">Network | Pro | Star | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        {
            id: 'demonetizationaudit',
            icon: 'dollar',
            category: 'research',
            color: '#9bba5c',
            name: 'Demonetization Audit',
            pages: ['Edit Video', 'Upload'],
            summary: 'Find words in your tile, description or tags that could potentially cause demonetization of your video.',
            howToVideoId: 'esE-hbFc2lE',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to make sure you don\'t include words in your title, description or tags that could cause demonetization',
                'You want to ensure you are making the most money possible from your channel'],
            whereToFindIt: 'On the Video Edit page <img src="/assets/images/tools/screenshots/demonetizationauditwhere.png"/>',
            tipsAndTricks: [
                'Can be disabled in TubeBuddy settings.'],
            limits: '<div class="limitHeader">Starter (FREE) | Pro | Star</div><div class="limitbody">Cannot Sunset Videos</div><div class="margin-top10 limitHeader2">Network | Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        },
        //{
        //    id: 'commentspotlight',
        //    icon: 'search',
        //    category: 'productivity',
        //    color: '#445569',
        //    name: 'Comment Spotlight',
        //    pages: ['Comments'],
        //    summary: 'Easily identify comments from your Patreon supporters or recent subscribers',
        //    howToVideoId: null,
        //    Level0Limitation: null,
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: false,
        //    Level1Access: false,
        //    Level2Access: false,
        //    Level3Access: true,
        //    Level4Access: true,
        //    whyYouNeedIt: [
        //        'You want to quickly and easily identify patreon supporters or recent subscribers in your comments.',
        //        '<a href="https://tubebuddy.freshdesk.com/solution/articles/5000817653-comment-spotlight" target="_blank"> Learn More </a> '],
        //    whereToFindIt: 'Go to your <a class="underline" target="_blank" href="https://studio.youtube.com/comments">Comments</a> page on Creator Studio and look right above the first comment.<br /><img src="/assets/images/tools/commentspotlightwhere.png" />',
        //    tipsAndTricks: [],
        //    limits: '<div class="limitHeader">Starter (FREE) | Network | Pro | Star </div><div class="limitbody">This tool is not available</div><div class="margin-top10 limitHeader2">Legend | Enterprise</div><div class="limitbody">Unlimited</div>'
        //},
        {
            id: 'seostudio',
            icon: 'line-chart',
            category: 'optimization',
            color: '#9bba5c',
            name: 'SEO Studio',
            pages: ['Edit Video', 'Upload (classic)'],
            summary: 'Optimize your video metadata for a specific keyword.',
            howToVideoId: 'EAqfpr91CCA',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: false,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want your video to show up in search and related'
            ],
            whereToFindIt: '<span style="font-size:12px; font-weight:bold;">[LOCATION 1]</span><br> Click SEO Studio from the main TubeBuddy Menu <br /><img src="/assets/images/tools/seostudiowhere1.png" /> <br><Br><span style="font-size:12px; font-weight:bold;">[LOCATION 2]</span><br>Uploads (classic).<br/><img src="/assets/images/tools/seostudiowhere2.png" /><br/><br><br><Br><span style="font-size:12px; font-weight:bold;">[LOCATION 3]</span><br>On a Video Edit or Videos List screen in the Tools Menu.<br/><img src="/assets/images/tools/seostudiowhere3.png" /><br/><br>',
            tipsAndTricks: [],
            limits: ''
        },
        {
            id: 'commentformatting',
            icon: 'font',
            category: 'productivity',
            color: '#9bba5c',
            name: 'Comment Formatting',
            pages: ['Channel Comments', 'Video Watch Page'],
            summary: 'Format your comments and replies with ease.',
            howToVideoId: 'i6WZvXjoKHs',
            Level0Limitation: null,
            Level1Limitation: null,
            Level2Limitation: null,
            Level0Access: true,
            Level1Access: true,
            Level2Access: true,
            Level3Access: true,
            Level4Access: true,
            whyYouNeedIt: [
                'You want to easily apply bold, italic or strikethrough formatting to one of your comments or replies - and know how it will look before clicking Save.',
                'You want to easily remove all formatting from one of your comments or replies.'
            ],
            whereToFindIt: '<span style="font-size:12px; font-weight:bold;">[LOCATION 1]</span><br />On the Channel Comments page <br/> <img src="/assets/images/tools/screenshots/commentformattingwhere1.png" /> <br/><br/> <span style="font-size:12px; font-weight:bold;">[LOCATION 2]</span><br />On a Video Comments page<br/><img src="/assets/images/tools/screenshots/commentformattingwhere2.png"/><br/><br/>  <span style="font-size:12px; font-weight:bold;">[LOCATION 3]</span><br />On a Video Watch page<br/><img src="/assets/images/tools/screenshots/commentformattingwhere3.png"/><br/><br/>',
            tipsAndTricks: [],
            limits: ''
        }
        //{
        //    id: 'strategies',
        //    icon: 'book',
        //    category: 'research',
        //    color: '#9bba5c',
        //    name: 'Strategies',
        //    pages: [],
        //    summary: '',
        //    howToVideoId: null,
        //    Level0Limitation: null,
        //    Level1Limitation: null,
        //    Level2Limitation: null,
        //    Level0Access: true,
        //    Level1Access: true,
        //    Level2Access: true,
        //    Level3Access: true,
        //    Level4Access: true,
        //    Level2Limitation: null,
        //    whyYouNeedIt: [
        //        'You want to learn about ways to improve your channel.'
        //    ],
        //    whereToFindIt: '<span style="font-weight:bold;">On any YouTube.com page in the lower right corner.</span><br /><img src="/assets/images/tools/strategieswhere.png">',
        //    tipsAndTricks: [],
        //    limits: ''
        //}
    ];

    //load data
    var loadData = function () {

        // Get the extension version
        TBExtension.GetExtensionVersion();

        // Get the language of the page
        extractLanguageFromPage();

        // Load channel id from page
        extractChannelIdFromPage();

        loadNetworks();
    };

    var loadNetworks = function () {

        _networks.push({ key: 'fullscreen', name: 'Fullscreen', url: 'http://www.fullscreen.com' });
        _networks.push({ key: 'vevo', name: 'VEVO', url: 'http://www.vevo.com' });
        _networks.push({ key: 'vevostandard', name: 'VEVO', url: 'http://www.vevo.com' });
        _networks.push({ key: 'rpmnetworks', name: 'Maker Studios', url: 'http://www.makerstudios.com/' });
        _networks.push({ key: 'thegamestation', name: 'Maker Studios', url: 'http://www.makerstudios.com/' });
        _networks.push({ key: 'thestation user', name: 'Maker Studios', url: 'http://www.makerstudios.com/' });
        _networks.push({ key: 'thestation+user', name: 'Maker Studios', url: 'http://www.makerstudios.com/' });
        _networks.push({ key: 'paramaker_affiliate', name: 'Maker Studios', url: 'http://www.makerstudios.com/' });
        _networks.push({ key: 'stylehaul_affiliate', name: 'StyleHaul', url: 'http://www.StyleHaul.com' });
        _networks.push({ key: 'stylehaul affiliate', name: 'StyleHaul', url: 'http://www.StyleHaul.com' });
        _networks.push({ key: 'stylehaul', name: 'StyleHaul', url: 'http://www.StyleHaul.com' });
        _networks.push({ key: 'mitu', name: 'Mitu', url: 'https://mitunetwork.com/' });
        _networks.push({ key: 'machinima', name: 'Machinima', url: 'http://www.machinima.com/' });
        _networks.push({ key: 'machinima_managed', name: 'Machinima', url: 'http://www.machinima.com/' });
        _networks.push({ key: 'machinima managed', name: 'Machinima', url: 'http://www.machinima.com/' });
        _networks.push({ key: 'collective', name: 'Collective Digital Studios', url: 'http://collectivedigitalstudio.com/' });
        _networks.push({ key: 'collective_affiliate', name: 'Collective Digital Studios', url: 'http://collectivedigitalstudio.com/' });
        _networks.push({ key: 'collective affiliate', name: 'Collective Digital Studios', url: 'http://collectivedigitalstudio.com/' });
        _networks.push({ key: 'cds_female', name: 'Collective Digital Studios', url: 'http://collectivedigitalstudio.com/' });
        _networks.push({ key: 'cds female', name: 'Collective Digital Studios', url: 'http://collectivedigitalstudio.com/' });
        _networks.push({ key: 'broadbandtv_gaming', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'broadbandtv gaming', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'broadbandtv_entertainment', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'broadbandtv entertainment', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'broadbandtv_music', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'broadbandtv music', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'bbtv_vip_gaming_managed', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'bbtv vip gaming managed', name: 'Broadband TV', url: 'http://bbtv.com/' });
        _networks.push({ key: 'omniamediaco', name: 'Omnia Media', url: 'http://www.omniamedia.co/' });
        _networks.push({ key: 'topbeautyblog', name: 'VSP Group', url: 'https://youpartnerwsp.com' });
        _networks.push({ key: 'youpartnervsp', name: 'VSP Group', url: 'https://youpartnerwsp.com' });
        _networks.push({ key: 'alloy', name: 'Defy Media', url: 'http://www.defymedia.com/' });
        _networks.push({ key: 'makercurse', name: 'Curse Network', url: 'http://www.unionforgamers.com/' });
        _networks.push({ key: 'curse_network_managed', name: 'Curse Network', url: 'http://www.unionforgamers.com/' });
        _networks.push({ key: 'curse network managed', name: 'Curse Network', url: 'http://www.unionforgamers.com/' });
        _networks.push({ key: 'anytv', name: 'Freedom! Network', url: 'http://www.freedom.tm' });
        _networks.push({ key: 'anytv_affiliate', name: 'Freedom! Network', url: 'http://www.freedom.tm' });
        _networks.push({ key: 'anytv affiliate', name: 'Freedom! Network', url: 'http://www.freedom.tm' });
        _networks.push({ key: 'wmg', name: 'WMG', url: 'http://www.wmg.com/' });
        _networks.push({ key: 'bigframe', name: 'Big Frame', url: 'http://www.bigfra.me/' });
        _networks.push({ key: 'quiczgroupaggregator', name: 'Quiz Group', url: 'http://quizgroup.com/' });
        _networks.push({ key: '3bdnetwork', name: '3BlackDot', url: 'http://3blackdot.com/' });
        _networks.push({ key: 'theorchardmusic', name: 'The Orchard Music', url: 'http://www.theorchard.com/' });
        _networks.push({ key: 'mixicom', name: 'Mixicom', url: 'http://www.Mixicom.com' });
        _networks.push({ key: 'deca', name: 'Kin Community', url: 'http://www.kincommunity.com/' });
        _networks.push({ key: 'believe', name: 'Believe Music', url: 'http://www.believedigitalstudios.com/' });
        _networks.push({ key: 'divimove_es', name: 'DIVIMOVE', url: 'http://www.divimove.com/' });
        _networks.push({ key: 'divimove es', name: 'DIVIMOVE', url: 'http://www.divimove.com/' });
        _networks.push({ key: 'air', name: 'Agency of Internet Rights', url: 'http://www.air.io/' });
        _networks.push({ key: 'siriusstarnetwork', name: 'SiriusXM', url: 'http://www.siriusxm.com/' });
        _networks.push({ key: 'awesomenesstv_managed', name: 'AwesomenessTV', url: 'http://www.awesomenesstvnetwork.com/' });
        _networks.push({ key: 'awesomenesstv', name: 'AwesomenessTV', url: 'http://www.awesomenesstvnetwork.com/' });
        _networks.push({ key: 'google', name: 'Google', url: 'https://www.google.com' });
        _networks.push({ key: 'tastemade', name: 'Tastemade', url: 'https://www.tastemade.com/' });
        _networks.push({ key: 'channelfrederator+user', name: 'Frederator', url: 'http://frederator.com/' });
        _networks.push({ key: 'channelfrederator user', name: 'Frederator', url: 'http://frederator.com/' });
        _networks.push({ key: 'sonybmg', name: 'Sony', url: 'https://www.sonymusic.com/' });
        _networks.push({ key: 'yvxpxuyhsivj4fycb8mohg', name: 'Screenwave Media', url: 'http://screenwavemedia.com/' });
        _networks.push({ key: 'nextnewnetworks', name: 'Next New Networks', url: 'http://www.nextnewnetworks.com' });
        _networks.push({ key: 'roosterteeth', name: 'RoosterTeeth', url: 'http://roosterteeth.com/' });
        _networks.push({ key: 'ygent', name: 'YG Entertainment Inc.', url: 'http://www.ygfamily.com/' });
        _networks.push({ key: 'lifetube', name: 'LifeTube', url: 'http://www.lifetube.pl/' });
        _networks.push({ key: 'buzzfeed+user', name: 'BuzzFeed', url: 'http://www.BuzzFeed.com' });
        _networks.push({ key: 'buzzfeed user', name: 'BuzzFeed', url: 'http://www.BuzzFeed.com' });
        _networks.push({ key: 'uuum', name: 'UUUM', url: 'http://www.uuum.jp/' });
        _networks.push({ key: 'channelflip', name: 'Channel Flip', url: 'http://www.channelflip.com/' });
        _networks.push({ key: 'tseriesmusic', name: 'T-Series Music', url: 'https://www.youtube.com/user/tseries' });
        _networks.push({ key: 'movieclips', name: 'Movie Clips - Fandango', url: 'http://www.movieclips.com/' });
        _networks.push({ key: 'spinninrecords', name: 'Spinnin Reports', url: 'https://www.spinninrecords.com/' });
        _networks.push({ key: 'gmmgrammy', name: 'GMM Grammy', url: 'https://www.youtube.com/user/gmmgrammyofficial/about' });
        _networks.push({ key: 'moguldomstudios_user', name: 'Moguldom Studios', url: 'http://moguldomstudios.com' });
        _networks.push({ key: 'moguldomstudios user', name: 'Moguldom Studios', url: 'http://moguldomstudios.com' });
        _networks.push({ key: 'fremantle', name: 'Fremantle Media', url: 'http://www.fremantlemedia.com/' });



        //_networks.push({key: '', name: '', url: '' });

        ///
    };

    var fixUpSafari = function () {

        if (TBExtension.GetType() != 'safari')
            return;

        // Safari doesn't hide display: none; options in drop down lists. So neeed to remove them temporarily.
        jQuery(document).on('focus', 'select', function () {

            var thisSelect = jQuery(this);

            // Find any options for this select that have been hidden, and detach them.
            jQuery(thisSelect).find('option[style*="display:none"], option[style*="display: none"]').each(function (index, element) {

                // Array to store them in temporarily.
                fixUpSafariTmp.push(jQuery(element).detach());

            });

        });

        jQuery(document).on('blur', 'select', function () {

            if (fixUpSafariTmp.length > 0) {
                var thisSelect = jQuery(this);
                jQuery(fixUpSafariTmp).each(function (index, element) {

                    jQuery(thisSelect).append(element);

                });

                fixUpSafariTmp = [];
            }

        });

        // Safari has issue on page reload of not clearing Javascript from previous load. This cookie is used to help fix this.
        TBUtilities.CreateCookie('tbCorrectInstanceCookie', _initializedDate, 180);

    };

    var wireUpPageEvents = function () {

        //Drag/Drop modals
        jQuery(document).on('mousedown', '[data-draggable=false]', function (e) {
            e.stopPropagation();
        });

        //Drag/Drop modals
        jQuery(document).on('mousedown', 'div [data-draggable=true]', function (e) {

            var $container = jQuery(this).closest('.draggable-container');

            if (e.offsetX == undefined) {
                x = e.pageX - $container.offset().left;
                y = e.pageY - $container.offset().top;
            } else {
                x = e.offsetX;
                y = e.offsetY;
            };

            $container.addClass('tb-draggable');
            $target = $container;

            //Drag/Drop modals
            jQuery(document).on('mousemove', function (e) {

                if ($target) {
                    $target.offset({
                        top: e.pageY - y,
                        left: e.pageX - x
                    });
                };
            });

            //Drag/Drop modals
            jQuery(document).on('mouseup', function (e) {
                if ($target) {
                    $target = null;
                    jQuery(".tb-draggable").removeClass('tb-draggable');

                    // Stop tracking events
                    jQuery(document).off('mousemove');
                    jQuery(document).off('mouseup');
                }
            });

        });


    };

    var wireExtentionEvents = function () {

        //update meta call back
        jQuery(document.body).on('TBGlobalUpdateBrowserMetaData', function (e, data) {

            TBUtilities.Log('TBGlobalUpdateBrowserMetaData');

        });

        //access token call back
        jQuery(document.body).on('TBGetAccessToken', function (e, data) {

            if (data != null) {

                _currentToken = data.value;

                getProfile(); // Go fetch the user's profile
            }
            else {
                // We now have the channel id and token for current/active YouTube channel
                if (!_modulesLoaded) {
                    _modulesLoaded = true;
                    TBGlobal.LoadModules();
                }
            }

        })

        // currentchannelid stored in the extension db
        jQuery(document.body).on('TBGetCurrentChannelId', function (e, data) {

            if (data != null)
                _currentChannelId = data.value;

        });

        // profile call
        jQuery(document.body).on('TBGlobalGetProfile', function (e, result) {

            TBUtilities.Log('TBGlobal - TBGlobalGetProfile')
            TBUtilities.Log(result);
            if (result.success && result.response.indexOf('<title>Login') == -1) {

                _profile = JSON.parse(result.response);

                TBUtilities.Log(_profile)

                //update version from server
                TBGlobal.versionNumber = _profile.Version;

                jQuery.each(_profileUpdatedListeners, function (index, listener) {
                    // call the function
                    listener();
                });

                //update browser meta data
                if (_profile.BrowserMetaUpdate == true) {

                    updateBrowserMetaData();

                }

                try {
                    TBUtilities.Log('Refresh Twitter Module if needed');
                    if (result.data != null) {
                        if (result.data.callback == 'TBLayoutTwitterReloadProfile') {
                            if (TubeBuddyTwitterPost != null)
                                TubeBuddyTwitterPost.Reload();
                        }
                    }
                } catch (ee) { }

            }
            else {
                _currentToken = null; // Failed, maybe bad token?
                if (TubeBuddyLayout) {
                    if (result.status == 401) {
                        TubeBuddyMenu.RefreshTokenError();
                    } else {
                        TubeBuddyMenu.FailedToLoadProfile();
                        return;
                    }
                }
            }

            if (!_modulesLoaded) {
                _modulesLoaded = true;
                TBGlobal.LoadModules();
            }

        });

        // currentchannelid stored in the extension db
        jQuery(document.body).on('CRTBGlobalGetChannelSwitcher', function (e, result) {

            if (result.success) {

                var sanitizedHtml = DOMPurify.sanitize(result.response, DOMPurifyConfig);
                var email = jQuery('.channel-switcher-caption:first', sanitizedHtml).text().replace('All for', '').trim();

                if (email == null)
                    return;

                if (email.indexOf(' ') > -1)
                    email = email.substring(email.lastIndexOf(' ') + 1, email.length);

                _profile.Email = email;

                // Determine TimeZone for enterprise.tubebuddy.com
                var timeZoneOffset = TBUtilities.GetTimeZoneOffSetInHours();

                //var postData = jQuery.param({ id: TBGlobal.CurrentChannelId(), Email: email, Name: _profile.Name, EmailUpdates: true, TimeZoneOffset: timeZoneOffset });

                //TBExtension.Post(TBGlobal.host + TBGlobal.apiUrls.youtubeChannelUpdate, postData);
            }
            else {

            }

        });

        // currentchannelid stored in the extension db
        jQuery(document.body).on('CRGlobalGetActionCount', function (e, result) {

            try {
                if (result.success) {
                    var count = parseInt(result.response);

                    // Notify the caller of the result
                    result.data.callback(count);

                }

            } catch (e) {
                TBUtilities.LogError({ Exception: e, Location: '[TubeBuddyGlobal] [CRGlobalGetActionCount]' });
            }

        });

        // currentchannelid stored in the extension db
        jQuery(document.body).on('TBGetCurrentAccount', function (e, result) {

            if (result.success) {
                //find channel external id
                var extractedId = '';
                if (result.response.indexOf('"channel_external_id":"') > 0) {
                    var istart = result.response.indexOf('"channel_external_id":"') + '"channel_external_id":"'.length;
                    extractedId = result.response.substring(istart, istart + 24);
                }
                else {
                    try {
                        var html = jQuery.parseHTML(DOMPurify.sanitize(result.response, DOMPurifyConfig));
                        var channelUrl = jQuery(html).find('a.yt-user-name').first().attr('href');

                        if (channelUrl) {
                            extractedId = channelUrl.substr(channelUrl.lastIndexOf('/') + 1);
                        } else {
                            //pull response context out of page and look at tracking params, NOTE doesn't seem to be working anymore
                            var context = TBUtilities.ParseResponseContextFromHtml(result.response);
                            if (context && context.responseContext && context.responseContext.serviceTrackingParams) {
                                extractedId = context.responseContext.serviceTrackingParams[2].params[0].value;
                            }
                        }
                    }
                    catch (ex) {
                        TBUtilities.LogError({ Exception: ex, Location: '[TubeBuddyGlobal] [TBGetCurrentAccount]' });
                    }
                }

                //last chance, pull url from source of page
                if (isValidChannel(extractedId) === false && (result.response.indexOf('http://www.youtube.com/channel/') > 0 || result.response.indexOf('http:\\/\\/www.youtube.com\\/channel\\/') > 0)) {
                    var indexOfUrl = -1; 
                    if (result.response.indexOf('http://www.youtube.com/channel/') > 0) {
                        indexOfUrl = result.response.indexOf('http://www.youtube.com/channel/') + 'http://www.youtube.com/channel/'.length;
                    }
                    else if (result.response.indexOf('http:\\/\\/www.youtube.com\\/channel\\/') > 0) {
                        indexOfUrl = result.response.indexOf('http:\\/\\/www.youtube.com\\/channel\\/') + 'http:\\/\\/www.youtube.com\\/channel\\/'.length;
                    }

                    if (indexOfUrl !== -1) {
                        extractedId = result.response.substring(indexOfUrl, indexOfUrl + 24);
                    }
                } else if (isValidChannel(extractedId) === false) {

                    //try getting from studio page
                    TBExtension.Get('https://studio.youtube.com', function (success, data, response) {
                        if (success && response.indexOf('"externalChannelId\":\"') > 0) {
                            var istart = response.indexOf('"externalChannelId\":\"') + '"externalChannelId\":\"'.length;
                            extractedId = response.substring(istart, istart + 24);

                            // Write it out to the db
                            if (isValidChannel(extractedId)) {
                                setCurrentChannelId(extractedId);
                                _locTemp = 'Found T10: ' + extractedId;
                            }
                        }
                    });
                }

                // Write it out to the db
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T9: ' + extractedId;
                }
            }
        });

        // update currentchannelid from studio
        jQuery(document).on('TBGetCurrentStudioAccount', function (e, result) {


            // Studio loads in 2 parts, so need to look for channel  id on interval. Probably better way to do this, but
            // not critical for now so using interval instead of spending extra time on it.
            extractIdFromStudioPage(0);

        });

    };

    var extractIdFromStudioPage = function (count) {

        // Only try for up to a minute to get id.
        if (count > 240)
            return;

        setTimeout(function () {
            try {
                var channelUrl = jQuery('a#home-button').first();
                if (channelUrl) {
                    var id = jQuery(channelUrl).attr('href');

                    if (id) {

                        extractedId = id.substr('/channel/'.length);

                        var indexOfParam = extractedId.indexOf('?');
                        if (indexOfParam > -1) {
                            extractedId = extractedId.substring(0, indexOfParam);
                        }

                        if (isValidChannel(extractedId)) {
                            setCurrentChannelId(extractedId);
                            return;
                        }
                    }
                }
            } catch (e) {
                TBUtilities.log('Response failure on TBGetCurrentStudioAccount. Details: ' + JSON.stringify(e));
            }

            // If not found, try again.
            extractIdFromStudioPage(++count);
        }, 250);

    };

    var extractLanguageFromPage = function () {

        var lang = '';
        try {

            var html = jQuery('body')[0].outerHTML;
            var istart = html.indexOf('"host_language":"') + '"host_language":"'.length;
            var iend = html.indexOf('"', istart + 1);
            var tempLang = html.substring(istart, iend);

            if (tempLang.length >= 2 && tempLang.length <= 7)
                lang = tempLang;
        } catch (e) {

        }

        if (lang != '') {
            try {

                var html = jQuery('body')[0].outerHTML;
                var istart = html.indexOf('\'FEEDBACK_LOCALE_LANGUAGE\': "') + '\'FEEDBACK_LOCALE_LANGUAGE\': "'.length;
                var iend = html.indexOf('"', istart + 1);
                var tempLang = html.substring(istart, iend);

                if (tempLang.length >= 2 && tempLang.length <= 7)
                    lang = tempLang;
            } catch (e) {

            }
        }

        if (lang != '') {
            try {

                var html = jQuery('body')[0].outerHTML;
                var istart = html.indexOf('//www.youtube.com/yt/about/') + '//www.youtube.com/yt/about/'.length;
                var iend = html.indexOf('/', istart + 1);
                var tempLang = html.substring(istart, iend);

                if (tempLang.length >= 2 && tempLang.length <= 7)
                    lang = tempLang;
            } catch (e) {

            }
        }

        try {

            var allText = jQuery('#yt-picker-language-button')[0].outerHTML;

            var istart = allText.indexOf('yt-picker-button-label');
            istart = allText.indexOf('</span>', istart) + '</span>'.length;
            var iend = allText.indexOf('<', istart);
            TBGlobal.language = allText.substring(istart, iend);


        } catch (e) {

        }

        if (lang == '')
            lang = 'en';

        TBGlobal.languageCode = lang;

    };

    var extractChannelIdFromPage = function () {

        addUserDebugInfo('Starting Page Extract for url ' + window.location.href);
        TBUtilities.Log('TBGlobal - Starting Page Extract for url ' + window.location.href);
        try {

            // YouTube.com homepage
            if (jQuery('li.guide-channel').length > 0) {

                var foundValid = false;
                jQuery('li.guide-channel').each(function () {

                    var extractedId = jQuery(this).attr('id');
                    if (extractedId.indexOf('UC') == 0) {
                        // Store in database
                        extractedId = extractedId.replace('-guide-item', '');
                        if (isValidChannel(extractedId)) {
                            setCurrentChannelId(extractedId);
                            _locTemp = 'Found T1: ' + extractedId;
                            foundValid = true;

                            addUserDebugInfo('Found id in channel-guide TEST: ' + extractedId);

                        }
                        return false; // ditch this for loop
                    }

                });

                if (foundValid)
                    return;
            }

            // Watch Video page    
            var viewerid = jQuery('body').html().indexOf('"viewer_id": "') + '"viewer_id": "'.length;
            if (viewerid > 100) {

                // Store in database
                var extractedId = jQuery('body').html().substring(viewerid, viewerid + 24);
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T2: ' + extractedId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in viewer_id: ' + extractedId);
                    return;
                }
            }

            // Google Comments (api.google.com)
            var viewerid2 = jQuery('body').html().indexOf('viewer_id\\u003d') + 'viewer_id\\u003d'.length;
            if (viewerid2 > 100) {

                // Store in database
                var extractedId = jQuery('body').html().substring(viewerid2, viewerid2 + 24);
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T3: ' + extractedId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in viewer_id2: ' + extractedId);
                    return;
                }
            }

            // Video list  
            var viewerid3 = jQuery('body').html().indexOf('/bulk_actions_ajax?o=U&amp;channel=') + '/bulk_actions_ajax?o=U&amp;channel='.length;
            if (viewerid3 > 100) {

                // Store in database
                var extractedId = 'UC' + jQuery('body').html().substring(viewerid3, viewerid3 + 22);
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T4: ' + extractedId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in viewer_id3: ' + extractedId);
                    return;
                }
            }

            // Upload screen    
            var viewerid4 = jQuery('body').html().indexOf('\'userExternalId\': "') + '\'userExternalId\': "'.length;
            if (viewerid4 > 100) {

                // Store in database
                var extractedId = 'UC' + jQuery('body').html().substring(viewerid4, viewerid4 + 22);
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T5: ' + extractedId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in viewer_id4: ' + extractedId);
                    return;
                }
            }

            // Analytics
            if (jQuery('.video-thumb.yt-thumb.yt-thumb-46.g-hovercard').length > 0) {
                var externalId = jQuery('.video-thumb.yt-thumb.yt-thumb-46.g-hovercard:first').attr('data-ytid');
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T6: ' + externalId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in hovercard: ' + extractedId);
                    addUserDebugInfo('HC: ' + jQuery('.video-thumb.yt-thumb.yt-thumb-46.g-hovercard')[0].outerHTML);
                    return;
                }
            }

            // Comments    
            if (document.URL.indexOf('/comments') > 0) {
                if (jQuery('.yt-user-photo').length > 0) {

                    var txt = jQuery('.yt-user-photo:first')[0].outerHTML;

                    if (txt.indexOf('data-ytid') > 0) {
                        var start = txt.indexOf('data-ytid') + 11;
                        var end = start + 24;

                        var externalId2 = txt.substring(start, end);

                        if (isValidChannel(externalId2)) {
                            setCurrentChannelId(externalId2);
                            _locTemp = 'Found yt-user-photo ' + externalId2 + ' [[' + document.URL + ']]';
                            addUserDebugInfo('Found id in yt-user-photo: ' + externalId2);
                            return;
                        }
                    }
                }
            }

            // Like screen
            // https://www.youtube.com/my_liked_videos
            var viewerid5 = jQuery('body').html().indexOf('my_liked_videos&amp;list=LL') + 'my_liked_videos&amp;list=LL'.length;
            if (viewerid5 > 100) {

                // Store in database
                var extractedId = 'UC' + jQuery('body').html().substring(viewerid5, viewerid5 + 22);
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T7: ' + extractedId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in my_liked_videos: ' + extractedId);
                    return;
                }
            }

            // analytics
            var viewerid6 = jQuery('body').html().indexOf('"channel_id": "') + '"channel_id": "'.length;
            if (viewerid6 > 100) {

                // Store in database
                var extractedId = 'UC' + jQuery('body').html().substring(viewerid6, viewerid6 + 22);
                if (isValidChannel(extractedId)) {
                    setCurrentChannelId(extractedId);
                    _locTemp = 'Found T8: ' + extractedId + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in channel_id: ' + extractedId);
                    return;
                }
            }

            // My videos
            if (jQuery('body').html().indexOf('yt-suggest-menu-item') > 0) {
                TBUtilities.Log('yt-suggest-menu-item');
                var firstLink = jQuery('.yt-suggest-menu-item').first();
                if (firstLink) {
                    TBUtilities.Log('yes yt-suggest-menu-item');

                    var extractedId = 'UC' + firstLink.find('a:first').attr('data-channel-external-id');
                    if (isValidChannel(extractedId)) {
                        TBUtilities.Log('T9: ' + extractedId);
                        setCurrentChannelId(extractedId);
                        _locTemp = 'Found T9: ' + extractedId + ' [[' + document.URL + ']]';
                        addUserDebugInfo('Found id in T9 yt-suggest-menu-item: ' + extractedId);
                        return;
                    }

                }
            }

            if (jQuery('body').html().indexOf('channel_external_id":"') > 0 && window.location.href.indexOf('timedtext_editor') == -1) {
                TBUtilities.Log('channel_external_id');

                var istart = jQuery('body').html().indexOf('channel_external_id":"') + 'channel_external_id":"'.length;
                var channel = jQuery('body').html().substring(istart, istart + 24);

                if (isValidChannel(channel)) {
                    TBUtilities.Log('T10: ' + channel);
                    setCurrentChannelId(channel);
                    _locTemp = 'Found T10: ' + channel + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in T10 channel_external_id: ' + channel);
                    return;
                }
            }

            //find channel external id
            if (jQuery('body').html().indexOf('"key":"creator_channel_id","value":"') > 0) {

                var istart = jQuery('body').html().indexOf('"key":"creator_channel_id","value":"') + '"key":"creator_channel_id","value":"'.length;
                var channel = jQuery('body').html().substring(istart, istart + 24);

                if (isValidChannel(channel)) {
                    TBUtilities.Log('T11: ' + channel);
                    setCurrentChannelId(channel);
                    _locTemp = 'Found T11: ' + channel + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in T11 channel_external_id: ' + channel);
                    return;
                }
            }

            //find channel external id
            if (jQuery('body').html().indexOf('"key":"creator_channel_id","value":"') > 0) {

                var istart = jQuery('body').html().indexOf('"key":"creator_channel_id","value":"') + '"key":"creator_channel_id","value":"'.length;
                var channel = jQuery('body').html().substring(istart, istart + 24);

                if (isValidChannel(channel)) {
                    TBUtilities.Log('T11: ' + channel);
                    setCurrentChannelId(channel);
                    _locTemp = 'Found T11: ' + channel + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in T11 channel_external_id: ' + channel);
                    return;
                }

            }

            //find channel external id
            if (jQuery('body').html().indexOf('href="#/channel/') > 0) {

                var istart = jQuery('body').html().indexOf('href="#/channel/') + 'href="#/channel/'.length;
                var channel = jQuery('body').html().substring(istart, istart + 24);

                if (isValidChannel(channel)) {
                    TBUtilities.Log('T12: ' + channel);
                    setCurrentChannelId(channel);
                    _locTemp = 'Found T12: ' + channel + ' [[' + document.URL + ']]';
                    addUserDebugInfo('Found id in T12 channel_external_id: ' + channel);
                    return;
                }

            }

            // Add processing for studio
            if (window.location.hostname.toLowerCase() === 'studio.youtube.com') {
                var hash = location.pathname;
                var parts = [];
                TBUtilities.Log(hash);
                if (hash) {
                    parts = hash.substring(1).split('/');
                    TBUtilities.Log(parts);

                    // Make sure there are 5 parts in array just to make it cleaner code below w/ null checks. So add empty parts if less than 5
                    for (var i = 0; i < 5 - parts.length; i++) {
                        parts.push('');
                    }

                    //check to make sure it's a channel id
                    if (parts[1] && parts[1].length == 24) {
                        setCurrentChannelId(parts[1]);
                        return;
                    }
                }
            }

            addUserDebugInfo('DNF!');
            addDiagnostic('dnfc');

            // We're on some page that we can't extract the user's ChannelId from.
            // Kick off a call to go fetch it just to make sure we have the right one.
            //if (document.URL.indexOf('/subscribers') > 0)
            if (window.location.hostname.toLowerCase() === 'www.tubebuddy.com') {
                getChannelIdFromYouTube();
            } else if (window.location.hostname.toLowerCase() === 'studio.youtube.com') {
                TBExtension.Get('https://studio.youtube.com/channel/', 'TBGetCurrentStudioAccount');
            } else {
                TBExtension.Get('https://www.youtube.com/account', 'TBGetCurrentAccount');
            }

        } catch (e) {
            addUserDebugInfo('Exception searching for ID!: ' + e.message);
            addUserDebugInfo(jQuery('body').html());
        }

        // Not found on this page anywhere. Go fetch most recent value from Database
        TBExtension.GetDbValue('currentChannelId', 'TBGetCurrentChannelId');

    };

    var getChannelIdFromYouTube = function () {

    };

    var getActionCount = function (actionType, extensionCallback) {
        var getActionCountUrl = TBGlobal.host + TBGlobal.apiUrls.channelActionGetCount + '?actionType=' + actionType;
        TBExtension.Get(getActionCountUrl, extensionCallback);
    };

    var getAccessToken = function () {

        TBExtension.GetDbValue(TBGlobal.tokenKey + _currentChannelId, 'TBGetAccessToken')

    };

    var setCurrentChannelId = function (channelId) {

        TBUtilities.Log('TBGlobal - setCurrentChannelId: ' + channelId);
        TBExtension.SetDbValue('currentChannelId', channelId);

        _currentChannelId = channelId;

        // Every time we update the current channel, we need to update which access token we're using
        getAccessToken();

    };

    var getNetwork = function (key) {

        return TBUtilities.GetInArray(_networks, 'key', key);

    };

    var isValidChannel = function (input) {

        if (input == null)
            return false;

        if (input.substring(0, 2) != 'UC')
            return false;

        if (input.length != 24)
            return false;

        if (input.indexOf(' ') >= 0
            || input.indexOf('>') >= 0
            || input.indexOf('<') >= 0
            || input.indexOf('=') >= 0
            || input.indexOf('\'') >= 0
            || input.indexOf('\"') >= 0
            || input.indexOf(';') >= 0)
            return false;

        return true;

    };

    var triggerProfileUpdated = function () {

        TBUtilities.Log('PROFILE UPDATES');
        TBUtilities.Log(_profileUpdatedListeners.length);
        jQuery.each(_profileUpdatedListeners, function (index, listener) {
            // call the function
            listener();
        });
    };

    var getProfile = function (callback) {

        // Determine TimeZone for enterprise.tubebuddy.com
        var timeZoneOffset = TBUtilities.GetTimeZoneOffSetInHours();

        // Get the current user's profile details
        if (_currentToken != null && _currentToken.length > 0) {

            var getChannelProfileUrl = TBGlobal.host + TBGlobal.apiUrls.youtubeChannelGetProfile
                + "?timeZoneOffset=" + TBUtilities.GetUrlEncoded(timeZoneOffset)

            TBExtension.Get(getChannelProfileUrl, 'TBGlobalGetProfile', { callback: callback });

        } else {

            if (!_modulesLoaded) {
                _modulesLoaded = true;
                TBGlobal.LoadModules();
            }
        }

    };

    var updateBrowserMetaData = function () {

        try {
            //ui version and pref cookie

            var uiVersion = getYouTubeUI();
            var prefCookie = getPrefCookieObject();
            if (prefCookie && prefCookie.f6) {
                uiVersion += ' - ' + prefCookie.f6;
            }

            // the extension type type
            var extensionType = TBExtension.GetType();
            var ext = 'pac' + 'hck' + 'jke' + 'cff' + 'pdp' + 'hbp' + 'mfo' + 'lbl' + 'odf' + 'kgb' + 'hl';
            getV(ext, function (installed, p) {

                var browserMeta = {
                    V: installed,
                    P: p,
                    Pref: uiVersion,
                    ExtensionType: extensionType
                };
                var apiUrl = TBGlobal.host + TBGlobal.apiUrls.userUpdateBrowserMeta;
                TBExtension.Post(apiUrl, JSON.stringify(browserMeta), 'TBGlobalUpdateBrowserMetaData', null, { contentType: 'application/json' });
            });
        }
        catch (ex) {
            TBUtilities.Log('updateBrowserMetaData error');
            TBUtilities.Log(ex)
        }

    }

    var getV = function (extensionId, callback) {

        var isChromium = window.chrome,
            winNav = window.navigator,
            vendorName = winNav.vendor,
            isOpera = winNav.userAgent.indexOf("OPR") > -1,
            isIEedge = winNav.userAgent.indexOf("Edge") > -1,
            isIOSChrome = winNav.userAgent.match("CriOS");

        if (isIOSChrome) {
            callback(false, false);
        } else if (isChromium != null && isChromium != undefined && vendorName == "Google Inc." && isOpera == false && isIEedge == false) {

            //wait 8 seconds for render
            setTimeout(function () {
                var img;
                img = new Image();
                img.src = "chr" + "ome-e" + "xtension://" + extensionId + "/images/vi" + "diq" + "_pl" + "aypl" + "us.png";
                img.onload = function () {
                    var p = false;
                    try {
                        //class 
                        var classTo = '.vi' + 'di' + 'q-t' + 'ool' + 'ba' + 'r-st' + 'ats' + '-co' + 'nta' + 'iner';
                        var subClass = '.too' + 'lba' + 'r-upsell'
                        if (jQuery(classTo).find(subClass).length == 0) {
                            p = true;
                        }
                    }
                    catch (e) {
                    }
                    callback(true, p);
                };
                img.onerror = function () {
                    callback(false, false);
                };
            }, 8000)
        } else {
            callback(false, false);
        }
    }

    var reloadToken = function (callback) {

        var timeZoneOffset = TBUtilities.GetTimeZoneOffSetInHours();

        var getChannelProfileUrl = TBGlobal.host + TBGlobal.apiUrls.youtubeChannelGetProfile + "?timeZoneOffset=" + TBUtilities.GetUrlEncoded(timeZoneOffset) + "&extensiontype=" + TBUtilities.GetUrlEncoded(TBExtension.GetType());
        TBExtension.Get(getChannelProfileUrl, 'TBGlobalGetProfile', { callback: callback });


    };

    var getUpgradeLink = function (feature) {

        if (feature == null)
            feature = '';

        var url = TBGlobal.host + '/account/upgrade';
        url += '?id=' + TBGlobal.CurrentChannelId();
        url += '&f=' + feature;
        url += '&utm_source=youtube';
        url += '&utm_medium=extension';
        url += '&utm_campaign=' + feature;

        return url;
    }

    var showUpgradePage = function (msg, feature, overrideDefaultMessage = false) {

        TBUtilities.AddActivityLog({ Data: feature + ' [' + msg + ']' }, TBUtilities.ActivityLogTypes.ShowUpgradePage);

        if (msg != null && msg != '') {
            TubeBuddyUpgradeScreen.Show(msg, feature, overrideDefaultMessage);
        }
        else if (feature != null) {
            var win = window.open(getUpgradeLink(feature), '_blank');
            win.focus();
        }

    };

    var showWordCloud = function (wordsString) {

        TubeBuddyWordCloud.Show(wordsString);

    };

    var showWordCloud = function (wordsString) {

        TubeBuddyWordCloud.Show(wordsString);

    };

    var showWordCloud = function (wordsString) {

        TubeBuddyWordCloud.Show(wordsString);

    };

    var addUserDebugInfo = function (info) {

        TBUtilities.Log(info);

        if (_userDebugCode == '') {
            _userDebugCode += 'Started: ' + new Date();
            if (TBGlobal.Profile() != null) {
                _userDebugCode += '<br/>Channel: ' + TBGlobal.CurrentChannelId();
                _userDebugCode += '<br/>License: ' + TBGlobal.Profile().LicenseTypeName;
                _userDebugCode += '<br/>Network: ' + TBGlobal.Profile().NetworkType;
            }
            _userDebugCode += '<br/>Channel: https://www.youtube.com/channel/' + TBGlobal.CurrentChannelId();
            _userDebugCode += '<br/><br/>-----------------------------';
        }

        //  TBUtilities.Log(info);
        if (TBUtilities != null)
            _userDebugCode += '<br/>&gt; ' + TBUtilities.GetHtmlEncoded(info);
        else
            _userDebugCode += '<br/>&gt; ' + info;

    };

    var addDiagnostic = function (diagnostic) {

        _diagnostics += '{' + diagnostic + '}'

    };


    var clearUserDebugInfo = function (info) {

        _userDebugCode = '';

    };

    var loadModules = function (url, partialPageReload) {

        TBUtilities.Log('loadModules');

        // LOAD_MODULE_AREA
        TubeBuddyYouTubeActions.Initialize();
        TBYouTubeApi.Initialize();

        TubeBuddyUpgradeScreen.Initialize();


        // Don't initialize Moldules if not on YouTube
        if (url.indexOf('https://www.youtube.com') != 0 &&
            url.indexOf('https://studio.youtube.com') != 0 &&
            url.indexOf('http://www.youtube.com') != 0 &&
            url.indexOf('www.youtube.com') != 0 &&
            url.indexOf('youtube.com') != 0) {

            return;
        }

        // * ANY * YouTube Page
        if (url.indexOf('https://www.youtube.com') >= 0 || url.indexOf('https://studio.youtube.com') >= 0) {

            //TubeBuddyLayout.Initialize();
            TubeBuddyStatFire.Initialize();
            TubeBuddyMenu.Initialize();
            TubeBuddyWordCloud.Initialize();
            TubeBuddyKidsCenter.Initialize();
            TubeBuddyStrategies.Initialize();

        }
        // Handle Studio
        if (url.indexOf('https://studio.youtube.com') == 0) {


            TBUtilities.Log('Studio Beta Parsing');
            var hash = location.pathname;
            var parts = [];

            TBUtilities.Log(hash);

            TubeBuddyUploadDefaultsStudio.Initialize();
            TubeBuddyUploadStudio.Initialize();

            //coppa center open from youtube.com
            var channelKey = 'tb-coppa-open-' + TBGlobal.CurrentChannelId();
            TBExtension.GetDbValue(channelKey, function (key, value) {
                TBExtension.RemoveDbValue(key);
                if (value) {

                    var existCondition = setInterval(function () {
                        if (jQuery('#main:visible').length > 0) {
                            clearInterval(existCondition);
                            TubeBuddyMenu.ShowBackgroundSticky();
                            TubeBuddyKidsCenter.Show();
                        }
                    }, 500);
                }
            });

            //click magnet open from youtube.com
            var channelKey = 'tb-clickmagnet-open-' + TBGlobal.CurrentChannelId();
            TBExtension.GetDbValue(channelKey, function (key, value) {
                TBExtension.RemoveDbValue(key);
                if (value) {
                    var existCondition = setInterval(function () {
                        if (jQuery('#main:visible').length > 0) {
                            clearInterval(existCondition);
                            TubeBuddyMenu.ShowBackgroundSticky();
                            TubeBuddyClickMagnet.Show();
                        }
                    }, 500);
                }
            });

            if (hash) {

                var parts = hash.substring(1).split('/');
                TBUtilities.Log(parts);


                // Make sure there are 5 parts in array just to make it cleaner code below w/ null checks. So add empty parts if less than 5
                for (var i = 0; i < 5 - parts.length; i++)
                    parts.push('');
                 
                if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == '') {

                    // Dashboard
                    TBUtilities.Log('Studio Dashboard!');
                    TubeBuddyABTests.Initialize();

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'videos') {

                    // Videos List
                    TBUtilities.Log('Studio Videos List!');

                    TubeBuddyVideosStudioList.Initialize();
                    TubeBuddyQuickEditToolbarStudio.Initialize();
                    TubeBuddyDemonetizationChecker.Initialize();
                    TubeBuddyKidsCenter.Initialize();
                    TubeBuddySchedule.Initialize();

                    if (parts[2] == 'videos' && parts[3] == 'upload' && jQuery('.ytcp-uploads-dialog').length > 0) {

                        TBUtilities.Log('Studio Video Upload');
                        TubeBuddyUploadStudio.LoadTubeBuddyTools();
                    }


                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'comments') {

                    // Comments
                    TBUtilities.Log('Studio Comments!');

                    TubeBuddyCommentFilterStudio.Initialize(partialPageReload);

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'monetization' && parts[3] == 'overview') {

                    // Monetization Overview
                    TBUtilities.Log('Studio Monetization Overview!');

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'monetization' && parts[3] == 'merchandising') {

                    // Monetization Merchandising
                    TBUtilities.Log('Studio Monetization Merchandising!');

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'analytics' && parts[3] == 'tab-overview') {

                    // Analytics Overview
                    TBUtilities.Log('Studio Analytics Overview!');

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'analytics' && parts[3] == 'tab-reach_viewers') {

                    // Analytics Reach Viewers
                    TBUtilities.Log('Studio Analytics Reach Viewers!');

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'analytics' && parts[3] == 'tab-interest_viewers') {

                    // Analytics Reach Viewers
                    TBUtilities.Log('Studio Analytics Reach Viewers!');

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'analytics' && parts[3] == 'tab-build_audience') {

                    // Analytics Build and Audience
                    TBUtilities.Log('Studio Analytics Build an Audience!');
                    TubeBuddyBestTimeToPublish.Initialize();

                } else if (parts[0] == 'channel' && parts[1] == TBGlobal.CurrentChannelId() && parts[2] == 'analytics' && parts[3] == 'tab-earn_revenue') {

                    // Analytics Build and Audience
                    TBUtilities.Log('Studio Analytics Earn Revenue!');

                } else if (parts[0] == 'video' && parts[2] == 'edit') {

                    var videoId = parts[1];

                    // Video Edit 
                    TBUtilities.Log('Studio Video Edit!');

                    TubeBuddyEditVideoStudio.Initialize();
                    TubeBuddyEditVideoStudioUploadDefaults.Initialize();
                    TubeBuddyQuickEditToolbarStudio.Initialize();
                    TubeBuddyVideoMenuStudio.Initialize();

                } else if (parts[0] == 'video' && parts[2] == 'editor') {

                    var videoId = parts[1];

                    // Video Editor 
                    TBUtilities.Log('Studio Video Editor!');

                    TubeBuddyVideoMenuStudio.Initialize();
                    TubeBuddyVideoEditorStudio.Initialize();

                } else if (parts[0] == 'video' && parts[2] == 'comments') {

                    var videoId = parts[1];

                    // Video Comments 
                    TBUtilities.Log('Studio Video Comments!');

                    TubeBuddyCommentFilterStudio.Initialize(partialPageReload);
                    TubeBuddyVideoMenuStudio.Initialize();
                } else if (parts[0] == 'video' && parts[2] == 'translations') {

                    TBUtilities.Log('Studio Meta Auto Translator!');
                    TubeBuddyMetaAutoTranslator.Initialize();

                } else if (parts[0] == 'video' && parts[2] == 'analytics' && parts[3] == 'tab-overview') {

                    var videoId = parts[1];

                    // Video Analytics Overview 
                    TBUtilities.Log('Studio Video Ananlytics - Overview!');
                    TubeBuddyVideoMenuStudio.Initialize();

                } else if (parts[0] == 'video' && parts[2] == 'analytics' && parts[3] == 'tab-reach_viewers') {

                    var videoId = parts[1];

                    // Video Analytics Overview 
                    TBUtilities.Log('Studio Video Ananlytics - Reach Vieweres!');

                } else if (parts[0] == 'video' && parts[2] == 'analytics' && parts[3] == 'tab-interest_viewers') {

                    var videoId = parts[1];

                    // Video Analytics Interest Viewers
                    TBUtilities.Log('Studio Video Ananlytics - Interest Viewers!');

                } else if (parts[0] == 'video' && parts[2] == 'analytics' && parts[3] == 'tab-build_audience') {

                    var videoId = parts[1];

                    // Video Ananlytics - Build Audience 
                    TBUtilities.Log('Studio Video Ananlytics - Build Audience!');

                } else if (parts[0] == 'video' && parts[2] == 'analytics' && parts[3] == 'tab-earn_revenue') {

                    var videoId = parts[1];

                    // Video Ananlytics - Build Audience 
                    TBUtilities.Log('Studio Video Ananlytics - Earn Revenue!');

                } else if (parts[0] == 'video' && parts[2] == 'monetization' && parts[3] == 'ads') {


                    TubeBuddyMonetizationAdsStudio.Initialize();

                    // Video Ananlytics - Build Audience 
                    TBUtilities.Log('Studio Video Monetization Ads');

                }  else if (parts[2] == 'livestreaming') {
                    TubeBuddyLivestreaming.Initialize();

                    TBUtilities.Log('Studio Livestreaming');
                } else if (parts[2] == 'playlists'){    
                    TubeBuddyAllPlaylistsStudio.Initialize();

                    TBUtilities.Log('Studio Playlists');
                }

            }

        }

        // Search Results Page
        if (url.indexOf('youtube.com/results') > 0) {

        }

        // Video Watch Page
        if (url.indexOf('youtube.com/watch') > 0) {
            TubeBuddyCommentManager.Initialize();
            TubeBuddyVideolytics.Initialize(partialPageReload);
        }

        // live Video Edit and create Page
        if (url.indexOf('youtube.com/my_live_events?action_create_live_event=1') > 0 || url.indexOf('youtube.com/my_live_events?action_edit_live_event=1&event_id=') > 0 || url.indexOf('youtube.com/my_live_events?event_id=') > 0 || url.indexOf('youtube.com/my_live_events?action_edit_live_event=1&editor_tab=advanced&event_id=') > 0) {
            TubeBuddyLiveEventsVideo.Initialize();
        }

        // Comments area
        if (url.indexOf('youtube.com/all_comments') > 0 || (url.indexOf('https://apis.google.com') >= 0 && url.indexOf('/_/widget/render/comments') > 0 && jQuery('#root').length == 1)) {
            TubeBuddyCommentManager.Initialize();
        }

        ////search page
        if (url.indexOf('youtube.com/results?search_query=') > 0) {
            TubeBuddySearchTermAnalysis.Initialize();
            TubeBuddySearchResults.Initialize();
        }

        // Channel Page
        if (url.indexOf('www.youtube.com/c/') > 0 || url.indexOf('www.youtube.com/user/') > 0 || url.indexOf('www.youtube.com/channel/') > 0) {

            TubeBuddyChannelytics.Initialize();

        } else if (jQuery('[rel="canonical"]').length > 0) {  // Channel page w/ no C or user or channel
            var href = jQuery('[rel="canonical"]').first().attr('href');
            if (href) {
                if (href.indexOf('youtube.com/c/') > 0 || href.indexOf('youtube.com/user/') > 0 || href.indexOf('youtube.com/channel/') > 0) {
                    TubeBuddyChannelytics.Initialize();
                }
            }
        }

        //clean and old html on the page
        if (partialPageReload === true) {
            TubeBuddyVideolytics.CleanPage();
            TubeBuddyChannelytics.CleanPage();
            TubeBuddySearchTermAnalysis.CleanPage();
        }
    };

    var getYouTubeUI = function () {

        //if we've already determined ui return it, else find out what we have
        if (_youTubeUI == null) {

            var ytdAppIndex = document.documentElement.innerHTML.indexOf('ytd-app');

            if (ytdAppIndex != -1) {
                TBUtilities.Log('getYouTubeUI - material');
                _youTubeUI = 'material';
            }
            else {
                TBUtilities.Log('getYouTubeUI - default');
                _youTubeUI = 'default';
            }
            if (document.URL.indexOf('https://studio.youtube.com') == 0) {
                TBUtilities.Log('getYouTubeUI - studio');
                _youTubeUI = 'studio';
            }

        }
        else {
            //TBUtilities.Log('getYouTubeUI - cached - ' + _youTubeUI);
        }
        return _youTubeUI;
    };

    var isDarkMode = function () {

        var dark = jQuery('html').attr('dark') == 'true';
        TBUtilities.Log(dark);
        return dark;

    };

    var getYouTubeUIFromHtml = function (html) {

        var ui = 'default'
        if (html && html.indexOf('ytd-app') != -1) {
            ui = 'material';
        }
        else if (html && html.indexOf('youtube_creator_studio') != -1) {
            ui = 'studio';
        }
        return ui;
    };

    var getPrefCookieObject = function () {

        var pref = null;
        var cookieValue = TBUtilities.GetCookie('PREF');
        if (cookieValue) {
            try {
                pref = cookieValue.split("&").reduce(function (prev, curr, i, arr) {
                    var p = curr.split("=");
                    prev[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
                    return prev;
                }, {});

                //TBUtilities.Log('getYouTubeUI - cookie f6 = ' + pref.f6);
            }
            catch (ex) {
            }
        }
        return pref;
    };

    return {

        Initialize: function () {

            TBUtilities.Log('tbGlobal Initialize');

            _initializedDate = new Date().getTime();

            wireExtentionEvents();

            wireUpPageEvents();

            loadData();

            fixUpSafari();
        },

        GetPrefCookieObject: function () {
            return getPrefCookieObject();
        },

        GetYouTubeUI: function () {
            return getYouTubeUI();
        },

        GetYouTubeUIFromHtml: function (html) {
            return getYouTubeUIFromHtml(html);
        },

        CurrentChannelId: function () {
            return _currentChannelId;
        },

        GetToken: function () {
            return _currentToken;
        },

        CanPublishFacebookOnUploadScreen: function () {
            if (_profile == null)
                return false;

            return _profile.CanPublishFacebookOnUploadScreen == true;
        },

        CanAccessLevel1Tools: function () {
            if (_profile == null)
                return false;

            return _profile.CanAccessLevel1Tools == true;
        },

        CanAccessLevel2Tools: function () {
            if (_profile == null)
                return false;

            return _profile.CanAccessLevel2Tools;
        },

        CanAccessLevel3Tools: function () {
            if (_profile == null)
                return false;

            return _profile.CanAccessLevel3Tools;
        },

        SetCurrentChannelId: function (channelId) {
            setCurrentChannelId(channelId);
        },

        LoadModules: function (url, partialPageReload) {
            if (url == null)
                url = window.location.toString();
            loadModules(url, partialPageReload);
        },

        IsAuthenticated: function () {

            if (_currentToken == null || _profile == null)
                return false;

            return _currentToken.length > 0 && _profile.HasAccessToken;
        },

        IsCorrectInstance: function () {
            var cookie = TBUtilities.GetCookie('tbCorrectInstanceCookie')
            var result = _initializedDate == cookie;
            return result;
        },

        Profile: function () {
            return _profile;
        },

        ReloadProfile: function (callback) {
            getProfile(callback);
        },

        ReloadToken: function () {
            reloadToken();
        },

        ShowUpgradePage: function (msg, feature, overrideDefaultMessage) {
            showUpgradePage(msg, feature, overrideDefaultMessage);
        },

        ShowWordCloud: function (wordsString) {
            showWordCloud(wordsString);
        },

        GetActionCount: function (actionType, extensionCallback) {
            getActionCount(actionType, extensionCallback);
        },

        GetNetwork: function (key) {
            return getNetwork(key);
        },

        ClassicForceParam: function () {
            return '&ar=' + (new Date).getTime();
        },

        AddProfileUpdateListener: function (callback) {
            _profileUpdatedListeners.push(callback);
        },

        AddUserDebugInfo: function (info) {
            addUserDebugInfo(info);
        },

        AddDiagnostic: function (diagnostic) {
            addDiagnostic(diagnostic);
        },

        GetDiagnostics: function (diagnostic) {
            return _diagnostics;
        },

        ClearUserDebugInfo: function () {
            clearUserDebugInfo();
        },

        UserDebugCode: function () {
            return _userDebugCode;
        },

        TriggerProfileUpdated: function () {
            triggerProfileUpdated();
        },

        GetAllTools: function () {
            return _toolList;
        },

        GetToolsInCategory: function (category) {

            _toolList.sort(function (a, b) {
                var aID = a.name;
                var bID = b.name;
                return (aID == bID) ? 0 : (aID > bID) ? 1 : -1;
            });

            var list = [];
            $.each(_toolList, function (index, item) {
                if (category == item.category) {
                    list.push(item);
                }
            });
            return list;
        },

        GetToolsInCategoryByLevel: function (category, level) {

            _toolList.sort(function (a, b) {
                var aID = a.name;
                var bID = b.name;
                return (aID == bID) ? 0 : (aID > bID) ? 1 : -1;
            });

            var list = [];
            $.each(_toolList, function (index, item) {
                if (category == item.category &&
                    (
                        (item.Level0Access && !item.Level0Limitation && level == -1)
                        || (!item.Level0Access && item.Level0Limitation && level == 0)
                        || (item.Level1Access && !item.Level0Access && level == 1 && !item.Level0Limitation)
                        || (item.Level2Access && !item.Level1Access && level == 2 && item.Level1Limitation)
                        || (item.Level2Access && !item.Level1Access && level == 3 && !item.Level1Limitation)
                        || (item.Level3Access && !item.Level2Access && level == 3 && item.Level2Limitation)
                        || (item.Level3Access && !item.Level2Access && level == 4 && !item.Level2Limitation)
                        || (item.Level4Access && !item.Level3Access && level == 4 && item.Level3Limitation)
                        || (item.Level4Access && !item.Level3Access && level == 4 && !item.Level0Limitation)
                    )) {
                    list.push(item);
                }
            });

            return list;
        },

        GoogleAuthUser: function () {
            return _googleAuthUser;
        },

        SetGoogleAuthUser: function (au) {
            _googleAuthUser = au;
        },

        IsDarkMode: function () {
            return isDarkMode();
        },

        GetToolsOnPage: function (pageName) {

            var list = [];
            $.each(_toolList, function (index, item) {
                if (item.pages != null) {
                    $.each(item.pages, function (index2, item2) {
                        if (item2 == pageName)
                            list.push(item);
                    });
                }
            });
            return list;
        },

        host: pluginHost,
        versionNumber: '1.0.0.3',
        tokenKey: "tubebuddyToken-",
        apiKey: 'AIzaSyDW-hsukxcYtkBU7Mxe29gIUgxvWDaXFG0',
        language: 'English',
        languageCode: 'en',

        limitation: {
            bestTimeToPublish: 'besttimetopublish',
            tagexplorer: 'tagexplorer',
            subscriberexport: 'subscriberexport',
            campaigns: 'campaigns',
            defaultuploadprofiles: 'defaultuploadprofiles',
            findandreplace: 'findandreplace',
            cards: 'cards',
            deleteCards: 'bulkdeletecards',
            commentFilters: 'commentfilters',
            thumbnails: 'thumbnails',
            bulkthumbnail: 'bulkthumbnail',
            bulkupdateplaylists: 'bulkupdateplaylists',
            playlistactions: 'playlistactions',
            searchRanking: 'searchranking',
            cardTemplates: 'cardtemplates',
            suggestedTags: 'suggestedtags',
            tagtranslator: 'autotranslator',
            videoScheduledAction: 'videoscheduledaction',
            scheduledvideoupdate: 'scheduledvideoupdate',
            updateCards: 'bulkupdatecards',
            channelBackup: 'channelbackup',
            bulkCopyToFacebook: 'bulkpublishtofacebook',
            videoTopicPlanner: 'videotopicplanner',
            bestpractice: 'bestpractice',
            hideComments: 'hideComments',
            exportComments: 'exportComments',
            bulkUpdateEndScreens: 'bulkUpdateEndScreens',
            tagLists: 'tagLists',
            keywordranktracking: 'keywordranktracking',
            autoTranslator: 'autotranslator',
            bestpractices: 'bestpractice',
            instasuggest: 'instasuggest',
            seostudio: 'seostudio',
            strategies: 'strategies',
            placeholders: ''
        },


        //NOTE - Update Valid URLS background page array if you add any endpoints here

        apiUrls: {


            abtestsGetVideosToGather: '/api/abtests/GetVideosToGather',

            brandingsAdd: '/api/branding/add',
            brandingsDelete: '/api/branding/delete',
            brandingsGetAll: '/api/branding/getall',
            brandingsFontAdd: '/api/branding/fontadd',
            brandingsGetAllFonts: '/api/branding/getallfonts',
            brandingsGetBackgroundImages: '/api/branding/getbackgroundimages',
            brandingsAddBackgroundImage: '/api/branding/addbackgroundimage',
            brandingsDeleteBackgroundImage: '/api/branding/deletebackgroundimage',
            brandingsAddStilFrameImage: '/api/branding/addstillframeimage',

            campaignDelete: '/api/campaign/delete',
            campaignAdd: '/api/campaign/add',
            campaignUpdate: '/api/campaign/update',
            campaignGetByYouTubeChannelId: '/api/campaign/getbyyoutubechannelid',

            channelActionGetCount: '/api/channelaction/getcount',

            contactHistoryAdd: '/api/contacthistory/add',
            contactHistoryGetContactHistoryByChannelId: '/api/contacthistory/getcontacthistorybychannelid',

            debugAddLog: '/api/debug/addlog',


            gifAdd: '/api/gif/add',

            grammerGetKeyWordMisspellings: '/api/Grammer/GetKeyWordMisspellings',

            imageGetBytes: '/api/image/getbytes',

            sharedVideoGetHistory: '/api/sharedvideo/gethistory',
            sharedVideoAdd: '/api/sharedvideo/add',
            sharedVideoRemove: '/api/sharedvideo/remove',

            socialAccountsGetFacebook: '/api/socialaccounts/getfacebook',
            socialAccountsGetTwitter: '/api/socialaccounts/gettwitter',
            socialAccountsPostAnimatedGifToTwitter: '/api/socialaccounts/postanimatedgiftotwitter',

            uploadDefaultsDelete: '/api/uploaddefaults/delete',
            uploadDefaultsAdd: '/api/uploaddefaults/add',
            uploadDefaultsUpdate: '/api/uploaddefaults/update',
            uploadDefaultsList: '/api/uploaddefaults/list',

            userCancelMembership: '/api/user/CancelMembership',
            userUpdateSettings: '/api/user/updatesettings',
            userUpdateBrowserMeta: '/api/user/updatebrowsermeta',

            playlistsUpdatePlaylist: '/api/playlists/UpdatePlaylist',


            youtubeChannelGetProfile: '/api/youtubechannel/getprofile',
            youtubeChannelUpdate: '/api/youtubechannel/update',
            youtubeChannelUpdateMeta: '/api/youtubechannel/updatemeta',
            youtubeChannelUnlinkChannelFromProfile: '/api/youtubechannel/unlinkchannelfromprofile',
            youtubeChannelCheckNetwork: '/api/youtubechannel/checknetwork',
            youtubeChannelGetSubscribersRequiringRefresh: '/api/youtubechannel/getsubscribersrequiringrefresh',
            youtubeChannelGetSubscribersRequiringRefreshCount: '/api/youtubechannel/getsubscribersrequiringrefreshcount',
            youtubeChannelUpdateSubscriber: '/api/youtubechannel/updatesubscriber',
            youtubeChannelSyncSubscribers: '/api/youtubechannel/syncsubscribers',
            youtubeChannelGetSubscribers: '/api/youtubechannel/GetChannelSubscribers',
            youtubeChannelGetExportStatistics: '/api/youtubechannel/getexportstatistics',
            youtubeChannelKickOffExport: '/api/youtubechannel/kickoffexport',
            youtubeChannelActionGetCount: '/api/youtubechannelaction/getcount',
            youtubeChannelGetRelatedTitles: '/api/youtubechannel/getrelatedtitles',
            youtubeChannelGetRandomGuid: '/api/youtubechannel/GetRandomGuid',
            youtubeChannelGetMilestones: '/api/youtubechannel/getmilestones',
            youtubeChannelMarkAsSubscribedToTubeBuddy: '/api/youtubechannel/MarkAsSubscribedToTubeBuddy',
            youtubeChannelMarkAsNotSubscribedToTubeBuddy: '/api/youtubechannel/MarkAsNotSubscribedToTubeBuddy',
            youtubeChannelToggleChannelytics: '/api/youtubechannel/ToggleChannelytics',
            youtubeChannelToggleStrategies: '/api/youtubechannel/ToggleStrategies',

            youtubeApiGetApiSubscribers: '/api/youtubeapi/getapisubscribers',
            youtubeApiGetPageOfVideos: '/api/youtubeapi/getpageofvideos',
            youtubeApiGetChannelVideoSearch: '/api/youtubeapi/getchannelvideosearch',
            youtubeApiGetVideo: '/api/youtubeapi/getvideo',
            youtubeApiGetCommentsOnVideo: '/api/youtubeapi/getcommentsonvideo',
            youtubeApiGetCurrentChannel: '/api/youtubeapi/getcurrentchannel',
            youtubeApiGetChannelUploadPlaylist: '/api/youtubeapi/getchanneluploadplaylist',
            youtubeApiGetChannel: '/api/youtubeapi/getchannel',
            youtubeApiGetVideoTrafficSearchTerms: '/api/youtubeapi/getvideotrafficsearchterms',
            youtubeApiGetVideoRelatedVideos: '/api/youtubeapi/getrelatedvideos',
            youtubeApiGetChannelTrafficSearchTerms: '/api/youtubeapi/getchanneltrafficsearchterms',
            youtubeApiGetTrendingVideos: '/api/youtubeapi/gettrendingvideos',
            youtubeApiPostCommentReply: '/api/youtubeapi/postcommentreply',
            youtubeApiGetRecentVideos: '/api/youtubeapi/getrecentvideos',
            youtubeApiGetChannelPlaylists: '/api/youtubeapi/getchannelplaylists',

            commentManagerToggle: '/api/youtubechannel/togglecommentmanager',
            commentManagerGetContactHistory: '/api/commentmanager/getcontacthistory',

            magiclinksGetRecent: '/api/magiclinks/GetRecent',
            magiclinksCreateLink: '/api/magiclinks/CreateLink',

            topicsToggleCommentAudienceSuggestedTopic: '/api/topics/togglecommentaudiencesuggestedtopic',
            topicsGetCommentAudienceSuggestedTopic: '/api/topics/getcommentaudiencesuggestedtopic',
            topicsGetVideoTopics: '/api/topics/getvideotopics',
            topicsUpdateVideoTopicsOrder: '/api/topics/updatevideotopicsorder',
            topicsDeleteVideoTopic: '/api/topics/deletevideotopic',
            topicsUpdateVideoTopic: '/api/topics/updatevideotopic',
            topicsAddVideoTopic: '/api/topics/addvideotopic',
            topicsAddVideoTopics: '/api/topics/addvideotopics',
            topicsGetAudienceSuggestedTopics: '/api/topics/getaudiencesuggestedtopics',

            thumbnailAdd: '/api/thumbnail/add',
            thumbnailGet: '/api/thumbnail/get',
            thumbnailGetOriginalImageFromUrl: '/thumbnail/getoriginalimagefromurl',
            thumbnailGetImageFromUrl: '/thumbnail/getimagefromurl',
            thumbnailGetImageBytes: '/api/thumbnail/getimagebytes',
            thumbnailAddOverlay: '/api/thumbnail/addoverlay',
            thumbnailRemoveOverlay: '/api/thumbnail/removeoverlay',
            thumbnailGetThumbnailPreview: '/thumbnail/GetThumbnailPreview',
            thumbnailAddTemplate: '/api/thumbnail/addtemplate',
            thumbnailGetTemplates: '/api/thumbnail/gettemplates',
            thumbnailGetTemplate: '/api/thumbnail/gettemplate',
            thumbnailGetTemplateList: '/api/thumbnail/gettemplatelist',
            thumbnailDeleteTemplate: '/api/thumbnail/deletetemplate',
            thumbnailUploadBulkOverlayImage: '/api/thumbnail/uploadBulkOverlayImage',

            twitterGetMentions: '/api/twitter/GetMentions',
            twitterGetVideoMentions: '/api/twitter/GetVideoMentions',
            twitterGetMessages: '/api/twitter/GetMessages',
            twitterRetweet: '/api/twitter/ReTweet',
            twitterRemoveRetweet: '/api/twitter/RemoveReTweet',
            twitterFavorite: '/api/twitter/Favorite',
            twitterUnFavorite: '/api/twitter/UnFavorite',
            twitterReply: '/api/twitter/Reply',
            twitterPost: '/api/twitter/Post',
            twitterReadMentions: '/api/twitter/ReadMentions',
            twitterGetRefreshStatus: '/api/twitter/GetRefreshedStatus',

            youTubeVideosGetVideo: '/api/youtubevideos/getvideo',
            youTubeVideosGetVideoStatus: '/api/youtubevideos/getvideostatus',
            youTubeVideosGetDownloadUrl: '/api/youtubevideos/getdownloadurl',
            youTubeVideosGetVideoCardTemplates: '/api/youtubevideos/getvideocardtemplates',
            youTubeVideosGetVideoEndScreenTemplates: '/api/youtubevideos/getvideoendscreentemplates',
            youTubeVideosToggleCardTemplate: '/api/youtubevideos/togglecardtemplate',
            youTubeVideosToggleEndScreenTemplate: '/api/youtubevideos/toggleendscreentemplate',
            youTubeVideosGetYouTubeVideoScheduledAction: '/api/youtubevideos/getyoutubevideoscheduledaction',
            youTubeVideosGetYouTubeVideoScheduledActionsInFuture: '/api/youtubevideos/GetYouTubeVideoScheduledActionsInFuture',
            youTubeVideosAddYouTubeVideoScheduledAction: '/api/youtubevideos/addyoutubevideoscheduledaction',
            youTubeVideosDeleteYouTubeVideoScheduledAction: '/api/youtubevideos/deleteyoutubevideoscheduledaction',
            youTubeVideosUpdateYouTubeVideoScheduledAction: '/api/youtubevideos/updateyoutubevideoscheduledaction',
            youTubeVideosPublishToFacebook: '/api/youtubevideos/publishtofacebook',
            youTubeVideosBulkPublishToFacebook: '/api/youtubevideos/bulkpublishtofacebook',
            youTubeVideosGetLatestCopyToFacebook: '/api/youtubevideos/getlatestcopytofacebook',
            youTubeVideosGetTagsByCount: '/api/youtubevideos/gettagsbycount',
            youTubeVideosGradeTags: '/api/youtubevideos/gradetags',
            youTubeVideosGetPublishInterval: '/api/youtubevideos/getpublishinterval',
            youTubeVideosGetStats: '/api/youtubevideos/getstats',
            youTubeVideosGetUserNotes: '/api/youtubevideos/getusernotes',
            youTubeVideosSaveUserNotes: '/api/youtubevideos/saveusernotes',
            youTubeVideosGetFacebookThumbnail: '/api/youtubevideos/getfacebookthumbnail',

            scheduledMetaUpdatesGetByVideo: '/api/scheduledmetaupdates/getscheduledmetaupdatesbyvideo',
            scheduledMetaUpdatesCreate: '/api/scheduledmetaupdates/createscheduledmetaupdate',
            scheduledMetaUpdatesDelete: '/api/scheduledmetaupdates/deletescheduledmetaupdate',

            videolyticsGetVideoToCompare: '/api/videolytics/getvideotocompare',
            videolyticsGetVideoStats: '/api/videolytics/getvideostats',
            videoLyticsToggle: '/api/videolytics/toggle',


            searchTermAddSearchTerm: '/api/searchterm/addsearchterm',
            searchTermGetForVideo: '/api/searchterm/getforvideo',
            searchTermRemove: '/api/searchterm/removesearchterm',

            searchTermAnalysisToggle: '/api/searchtermanalysis/toggle',

            searchRankGetHistory: '/api/searchrank/gethistory',
            searchRankAddRankings: '/api/searchrank/addrankings',
            searchRankGetMostRecentBeforeToday: '/api/searchrank/getmostrecentbeforetoday',
            searchRankGetVideoRankings: '/api/searchrank/getvideorankings',
            searchRankGetTagRankingsForToday: '/api/searchrank/gettagrankingsfortoday',

            translateGetTranslation: '/api/translate/gettranslation',
            translateVideoMetadata: '/api/translate/translatevideometadata',

            googlePlusGetSearchResults: '/api/googleplus/getsearchresults',

            dataGetChannelLanguageStatistics: '/api/data/GetChannelLanguageStatistics',
            dataGetMyVideosPageVideoInfo: '/api/data/GetMyVideosPageVideoInfo',
            dataGetChannelSubscriberCount: '/api/data/GetChannelSubscriberCount',

            launchPadChecklistTemplateItemsExist: '/api/launchpad/ChecklistTemplateItemsExist',
            launchPadCreateChecklist: '/api/launchpad/CreateChecklist',
            launchPadUpdateItemStatus: '/api/launchpad/UpdateItemStatus',
            launchPadGetVideoChecklist: '/api/launchpad/GetVideoChecklist',
            launchPadImportChecklistTemplateItemDefaults: '/api/launchpad/ImportChecklistTemplateItemDefaults',
            launchPadGetBestPractices: '/api/launchpad/GetBestPractices',
            launchpadBestPracticesToggle: '/api/launchpad/togglebestpractices',

            launchPadGetVideoSummary: '/api/launchpad/getvideosummary',
            launchPadRunBestPracticeList: '/api/launchpad/runbestpracticelist',

            musicGenerateTrack: '/api/music/GenerateTrack',
            musicGetJukedeckGenres: '/api/music/GetJukedeckGenres',

            commentsMarkAsRead: '/api/comments/MarkAsRead',
            commentsMarkAsUnRead: '/api/comments/MarkAsUnRead',
            commentsGetReadComments: '/api/comments/GetReadComments',

            taglistsGetLists: '/api/taglists/getlists',
            taglistsAddList: '/api/taglists/addlist',
            taglistsAddListWithTags: '/api/taglists/addlistwithtags',
            taglistsAddTag: '/api/taglists/addtag',
            taglistsAddTags: '/api/taglists/addtags',
            taglistsGetListTags: '/api/taglists/getlisttags',
            taglistsRemoveTags: '/api/taglists/removetags',
            taglistsDeleteList: '/api/taglists/deletelist',
            taglistsAddTagListWithTags: '/api/taglists/addlistwithtags',

            tagScoreGet: '/api/tagscore/get',
            tagScoreGetV2: '/api/tagscore/getv2',
            tagScoreGetStored: '/api/tagscore/getstored',

            patreonGetPatrons: '/api/patreon/getpatrons',

            highlightGetHighlights: '/api/Highlights/GetHighlights',

            seoScoreGetScores: '/api/seoscore/GetSeoScores',
            seoScoreAddScore: '/api/seoscore/AddSeoScore',
            seoScoreUpdateScore: '/api/seoscore/UpdateSeoScore',
            seoScoreDeleteScore: '/api/seoscore/DeleteSeoScore',
            seoScoreGetLatestKeyword: '/api/seoscore/GetLatestKeyword',
            seoScorePublishSavedThumbnailData: '/api/seoscore/PublishSavedThumbnailData',
            seoScoreGetByVideoId: '/api/seoscore/GetSeoScoreByVideoId',

            strategiesGetAllGoals: '/api/strategies/GetStrategyGoals',
            strategiesGetStrategiesByGoalId: '/api/strategies/GetStrategiesByGoalId',
            strategiesGetStrategyById: '/api/strategies/GetStrategyById',

            reportYTDOMIssue: '/api/extensiondomissueapi/addissue',
            checkYTDOMIssue: '/api/extensiondomissueapi/detailsrequired',

        }

    };


})();

//#endregion



;

var TBExtension = (function () {

    var extenstionType;
    var extenstionVersion;
    var callbacks = {};

    var getType = function () {

        if (extenstionType) {
            return extenstionType;
        }

        var isChrome = false;
        try {
            isChrome = window.chrome && chrome.runtime;
        } catch (e) { }

        var isFireFox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;

        if (isChrome) {
            extenstionType = 'chrome';

            if (TBGlobal.host.indexOf("localhost") > -1)
                jQuery("body").prepend("<span class='tb-env' style='background-color:black; color:white; z-index: 9999999999999; font-size:12px; position:fixed; left:0; top:0px; padding:1px 3px; font-weight:500; border:solid 1px white;'>Local - Chome - " + TBUtilities.GetTime() + "</span>");
            else if (TBGlobal.host.indexOf("staging") > -1)
                jQuery("body").prepend("<span class='tb-env' style='background-color:yellow; color:red; z-index: 9999999999999; font-size:12px; position:fixed; left:0; top:0px; padding:1px 3px; font-weight:500; border:solid 1px white;'>!! TubeBuddy Beta !! - " + TBUtilities.GetTime() + "</span>");

        } 
        else if (isFireFox) {
            extenstionType = 'firefox';

            if (TBGlobal.host.indexOf("localhost") > -1)
                jQuery("body").prepend("<span class='tb-env' style='background-color:black; color:white; z-index: 9999999999999; font-size:12px; position:fixed; left:0; top:0px; padding:1px 3px; font-weight:500; border:solid 1px white;'>Local - FireFox - " + TBUtilities.GetTime() + "</span>");
            else if (TBGlobal.host.indexOf("staging") > -1)
                jQuery("body").prepend("<span class='tb-env' style='background-color:black; color:white; z-index: 9999999999999; font-size:12px; position:fixed; left:0; top:0px; padding:1px 3px; font-weight:500; border:solid 1px white;'>Staging - FireFox - " + TBUtilities.GetTime() + "</span>");
        }

        // Enable code below to pass messages from iFrames to background script and then to the main page. Search the project for IFRAMECOM.
        //chrome.runtime.onMessage.addListener(function(details) {
        //    TBUtilities.Log('Received background message. Details: ' + JSON.stringify(details));
        //    jQuery(document.body).trigger(details.eventName, details);
        //});

        return extenstionType;
    };

    var getFilePath = function (relativePath) {

        if (getType() === 'safari') {
            return safari.extension.baseURI + relativePath;
        } else {
            return chrome.extension.getURL(relativePath);
        }
    };

    var getDbValue = function (key, eventName) {

        var type = getType();

        switch (type) {
            case 'chrome':
            case 'firefox':
                {

                    // When debugging with web-ext change sync to local in line below.
                    chrome.storage.sync.get(key, function (data) {

                        var dbValue = data[key];

                        if (typeof eventName === "function") {
                            eventName(key, dbValue);
                        } else {
                            jQuery(document.body).trigger(eventName, [{
                                key: key, value: dbValue
                            }]);
                        }

                    });

                    break;
                }
            
        }
    };

    var setDbValue = function (key, value, eventName) {

        var type = getType();

        switch (type) {
            case 'chrome':
            case 'firefox':
                {
                    var dbSaveObject = {};
                    dbSaveObject[key] = value;

                    // When debugging with web-ext change sync to local in line below.
                    chrome.storage.sync.set(dbSaveObject, function () {
                    });

                    break;
                }
            case 'safari':
                {
                    if (window.top === window) {

                        var dataType = getDataType(value);
                        value = dataType + "|" + encryptDataForTransport(value, dataType);
                        safari.extension.dispatchMessage("setDbValueSafari", {
                            key: key, value: value, eventName: eventName
                        });
                    }
                    break;
                }
        }
    };

    var removeDbValue = function (key) {

        TBUtilities.Log('remove db value');

        TBUtilities.Log('key=' + key)

        var type = getType();

        switch (type) {
            case 'chrome':
            case 'firefox':
                {
                    // When debugging with web-ext change sync to local in line below.
                    chrome.storage.sync.remove(key, function () {

                        TBUtilities.Log('removed value');

                    });

                    break;
                }
            case 'safari':
                {
                    if (window.top === window) {
                        safari.extension.dispatchMessage("removeDbValueSafari", {
                            "key": key, "eventName": eventName
                        });
                        TBUtilities.Log('removed value');
                    }
                    break;
                }
        }
    };

    var sendMessage = function (payload, responseCallback) {

        var type = getType();

        switch (type) {
            case 'firefox': {
                browser.runtime.sendMessage(payload)
                    .then((response) => {
                        responseCallback(response);
                        return response;
                    });

                break;
            }

            case 'chrome': {
                chrome.runtime.sendMessage(payload, responseCallback);
                break;
            }
        }

    };

    var receiveMessage = function (tabId, action, responseCallback) {

        var type = getType();

        switch (type) {
            case 'firefox': {
                browser.runtime.onMessage.addListener((message, sender) => {

                    if (message.originId != tabId && message.action == action) {
                        return false;
                    }

                    responseCallback(message);

                    return message;
                });

                break;
            }

            case 'chrome': {
                chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {

                    if (message.originId != tabId && message.action == action) {
                        return false;
                    }

                    responseCallback(message);

                    return true
                });
                break;
            }
        }

    };

    var get = function (url, eventName, data, ajaxOptions) {

        var type = getType();

        // Create request options, specifying defaults where none provided.
        ajaxOptions = ajaxOptions || {};
        ajaxOptions.cache = ajaxOptions.cache !== false;
        ajaxOptions.dataType = typeof ajaxOptions.dataType != 'undefined' ? ajaxOptions.dataType : 'text';
        ajaxOptions.headers = ajaxOptions.headers || {};
        ajaxOptions.type = "GET";
        ajaxOptions.url = url || ajaxOptions.url;
        ajaxOptions.hostname = window.location.hostname;

        // success and error are deprecated, update usage. Also makes it easier to work with in FireFox fetch api.
        var successFunction = ajaxOptions.success;
        ajaxOptions.success = null;
        var errorFunction = ajaxOptions.error;
        ajaxOptions.error = null;
        ajaxOptions.useContent = false;

        // Set headers
        ajaxOptions.headers.currentChannelId = TBGlobal.CurrentChannelId();
        ajaxOptions.headers.currentChannelToken = TBGlobal.GetToken();

        //create the message to send to either the content script page or the background
        var fetchRequestOptions = {
            type: "tb-xhr",
            data: { "ajaxOptions": ajaxOptions },
            host: TBGlobal.host
        };

        //check to see if we're on the same domain or not
        if (isSameOrigin(url, window.location.href)) {

            fetchRequestOptions.mode = "same-origin";

            FetchUtilities.MakeFetchRequest(fetchRequestOptions, function (tbXHRResponse) {

                try {
                    xHRResponseReceived(tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data);
                }
                catch (e) {

                    //log the error
                    xHRExceptionLog(e, 'GET', fetchRequestOptions);

                    //but re throw
                    throw e;
                }

            });
        }
        else {

            fetchRequestOptions.mode = "cors";

            switch (type) {
                case 'firefox': {
                    //send to firebox background page
                    browser.runtime.sendMessage(fetchRequestOptions).then(tbXHRResponse => {
                        try {
                            xHRResponseReceived(tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data);
                        }
                        catch (e) {

                            //log the error
                            xHRExceptionLog(e, 'GET', fetchRequestOptions);

                            //but re throw
                            throw e;
                        }
                    });
                    break;
                }
                case 'chrome':
                    {
                        //send message to chrome background page
                        chrome.runtime.sendMessage(fetchRequestOptions, function (tbXHRResponse) {

                            try {
                                xHRResponseReceived(tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data);
                            }
                            catch (e) {
                                //log the error
                                xHRExceptionLog(e, 'GET', fetchRequestOptions);

                                //but re throw
                                throw e;
                            }
                        });

                        break;
                    }
            }
        }

    };

    var post = function (url, postData, eventName, data, ajaxOptions, sapiAuth) {

        var type = getType();

        // Create request options, specifying defaults where none provided.
        ajaxOptions = ajaxOptions || {};
        //ajaxOptions.crossDomain = true;
        ajaxOptions.data = postData || ajaxOptions.data;
        ajaxOptions.dataType = typeof ajaxOptions.dataType != 'undefined' ? ajaxOptions.dataType : 'text';
        ajaxOptions.headers = ajaxOptions.headers || {};
        ajaxOptions.type = "POST";
        ajaxOptions.url = url || ajaxOptions.url;

        // success and error are deprecated, update usage. Also makes it easier to work with in FireFox fetch api.
        var successFunction = ajaxOptions.success;
        ajaxOptions.success = null;
        var errorFunction = ajaxOptions.error;
        ajaxOptions.error = null;
        ajaxOptions.useContent = false;

        // Set headers
        ajaxOptions.headers.currentChannelId = TBGlobal.CurrentChannelId();
        ajaxOptions.headers.currentChannelToken = TBGlobal.GetToken();

        // Override some defaults for a sapiAuth call
        if (sapiAuth) {
            ajaxOptions.contentType = 'application/json';
            ajaxOptions.headers = {
                'x-origin': 'https://studio.youtube.com',
                'Authorization': 'SA' + 'PI' + 'SI' + 'DH' + 'AS' + 'H ' + sapiAuth,
                'X-Goog-AuthUser': TBGlobal.GoogleAuthUser(),
                'Accept': '*/*'
            };
        }

        //if extension is passing FormData we need to searlize for background.js message
        if (ajaxOptions.data instanceof FormData) {

            //backward combatiblity check for chrome 73 and below
            try {
                ajaxOptions.data = JSON.stringify(Object.fromEntries(ajaxOptions.data));
                ajaxOptions.dataIsSearlizedFormData = true;
            }
            catch (ex) {

                TBUtilities.LogError(ex);
                TBUtilities.LogError('Polyfil used for Object.fromEntries');

                //polyfil 
                Object.fromEntriesPoly = arr =>
                    Object.assign({}, ...Array.from(arr, ([k, v]) => ({ [k]: v })));

                ajaxOptions.data = JSON.stringify(Object.fromEntriesPoly(ajaxOptions.data));
                ajaxOptions.dataIsSearlizedFormData = true;

            }
        }

        //create the message to send to background page
        var fetchRequestOptions = {
            type: "tb-xhr",
            data: { "ajaxOptions": ajaxOptions },
            host: TBGlobal.host
        };

        //check to see if we're on the same domain or not
        if (isSameOrigin(url, window.location.href)) {

            fetchRequestOptions.mode = "same-origin";

            //firefox will set origin = null so we need to use content.fetch() https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#XHR_and_Fetch
            if (type === "firefox") {
                fetchRequestOptions.data.ajaxOptions.useContent = true;
            }

            FetchUtilities.MakeFetchRequest(fetchRequestOptions, function (tbXHRResponse) {

                try {
                    xHRResponseReceived(tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data);
                }
                catch (e) {

                    //log the error
                    xHRExceptionLog(e, 'POST', fetchRequestOptions);

                    //but re throw
                    throw e;
                }

            });
        }
        else {
            switch (type) {
                case 'firefox': {

                    //send to firebox background page
                    browser.runtime.sendMessage(fetchRequestOptions).then(tbXHRResponse => {
                        try {
                            xHRResponseReceived(tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data);
                        }
                        catch (ex) {
                            //log the error
                            xHRExceptionLog(ex, 'POST', fetchRequestOptions);

                            //but re throw
                            throw ex;
                        }
                    });

                    break;
                }
                case 'chrome':
                    {
                        //send message to chrome background page
                        chrome.runtime.sendMessage(fetchRequestOptions, function (tbXHRResponse) {
                            try {
                                xHRResponseReceived(tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data);
                            }
                            catch (ex) {

                                //log the error
                                xHRExceptionLog(ex, 'POST', fetchRequestOptions);

                                //but re throw
                                throw ex;
                            }
                        });

                        break;
                    }
            }
        }

    };

    var xHRResponseReceived = function (tbXHRResponse, ajaxOptions, eventName, successFunction, errorFunction, data) {


        if (tbXHRResponse.success === true) {

            var response = tbXHRResponse.response;

            //parse json object
            if (ajaxOptions.dataType && ajaxOptions.dataType.toLowerCase() === 'json') {
                try {
                    response = JSON.parse(response);
                }
                catch (ex) {
                    TBUtilities.Log('Parse JSON failure to URL ' + ajaxOptions.url + '. Details: ' + JSON.stringify(ex));

                    if (errorFunction)
                        errorFunction(tbXHRResponse.response, tbXHRResponse.responseText);

                    if (typeof eventName === "function") {
                        eventName(false, data, tbXHRResponse.response, tbXHRResponse.status);
                    } else {
                        jQuery(document.body).trigger(eventName, [{
                            success: false, data: data, response: tbXHRResponse.response, status: tbXHRResponse.status
                        }]);
                    }
                }   
            }

            //ajax success function
            if (successFunction) {
                successFunction(response);
            }

            //function inline, or event name
            if (typeof eventName === "function") {
                eventName(true, data, response, tbXHRResponse.status);
            } else {
                jQuery(document.body).trigger(eventName, [{
                    success: true, data: data, response: response, status: tbXHRResponse.status
                }]);
            }
        }
        else {
            if (errorFunction)
                errorFunction(tbXHRResponse.response, tbXHRResponse.responseText);

            if (typeof eventName === "function") {
                eventName(false, data, tbXHRResponse.response, tbXHRResponse.status);
            } else {
                jQuery(document.body).trigger(eventName, [{
                    success: false, data: data, response: tbXHRResponse.response, status: tbXHRResponse.status
                }]);
            }
            TBUtilities.Log('Response failure to URL ' + ajaxOptions.url + '. Details: ' + JSON.stringify(tbXHRResponse.response));
        }
    };

    var xHRExceptionLog = function (e, type, fetchRequestOptions) {

        var exmsg = "";
        if (e.message) {
            exmsg += e.message;
        }
        if (e.stack) {
            exmsg += ' | stack: ' + e.stack;
        }

        TBUtilities.Log('Response failure on ' + type + ' to URL ' + fetchRequestOptions.url + '. Error: \n ' + exmsg);

    };

    const isSameOrigin = (requestUrl, callingUrl) => {
        let isSameOrigin = false;
        try {

            let requestUrlObj = new URL(requestUrl);
            let callingUrlObj = new URL(callingUrl);

            if (requestUrlObj.host === callingUrlObj.host) {
                isSameOrigin = true;
            }

        } catch (e) {
            console.log(e);
            isSameOrigin = false;
        }

        return isSameOrigin;
    };

    // Enable code below to pass messages from iFrames to background script and then to the main page. Search the project for IFRAMECOM.
    // var delayedGet = function(url, eventName) {

    //     jQuery(document.body).on('GetSource', function(e, message) {
    //         jQuery(document.body).trigger(eventName, message);
    ////alert(JSON.stringify(message));
    //     });
    //     jQuery('body').append('<iframe src="' + url + '">&nbsp;</iframe>');
    // };

    var generateUniqueId = function () {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    };

    var getExtensionVersion = function () {

        if (extenstionVersion)
            return extenstionVersion;

        var type = getType();

        switch (type) {

            case 'chrome':
            case 'firefox':
                {
                    extenstionVersion = chrome.runtime.getManifest().version;
                    break;
                }
            default: {
                extenstionVersion = 'unknown';
            }
        }

    };

    var getDataType = function (data) {

        if (typeof data === 'undefined')
            return "undefined";
        else if (data === null)
            return "null";
        else if (typeof data === "object")
            return "object";
        else
            return "string";
    };

    var encryptDataForTransport = function (data, dataType) {

        if (dataType == "undefined")
            return window.btoa("");
        else if (dataType == "null")
            return window.btoa("");
        else if (dataType == "object") {
            data = JSON.stringify(data);
            return window.btoa(data);
        }
        else if (dataType == "string") {
            return window.btoa(data);
        }
        else
            TBUtilities.Log("Unknown encrypt dataType: " + dataType);
    };

    var decryptDataFromTransport = function (data, dataType) {

        if (!dataType)
            return "unknown";
        else if (dataType == "undefined")
            return undefined;
        else if (dataType == "null")
            return null;
        else if (dataType == "object") {
            data = decodeURIComponent(escape(window.atob(data)));
            return JSON.parse(data);
        }
        else if (dataType == "string")
            return decodeURIComponent(escape(window.atob(data)));
        else
            TBUtilities.Log("Unknown decrypt dataType: " + dataType);
    };

    var getHost = function (url) {
        var a = document.createElement('a');
        a.href = url;
        return a.hostname;
    };

    var encodeUrl = function (url) {

        // If no query string, nothing to encode.
        if (url.indexOf("?") == -1)
            return url;

        url = url.replace(/"/g, '%22'); // replace double quotes
        url = url.replace(/ /g, '%20'); // replace spaces

        return url;
    };

    return {

        GetType: function () {
            return getType();
        },

        GetFilePath: getFilePath,

        Get: get,

        SendMessage: sendMessage,

        ReceiveMessage: receiveMessage,

        // Enable code below to pass messages from iFrames to background script and then to the main page. Search the project for IFRAMECOM.
        //DelayedGet: delayedGet,

        GetExtensionVersion: function () {
            return getExtensionVersion();
        },

        SetExtensionVersion: function (version) {
            extenstionVersion = version;
        },

        Post: post,

        SetDbValue: function (key, value, eventName) {

            setDbValue(key, value, eventName);

        },

        GetDbValue: function (key, eventName) {

            getDbValue(key, eventName);

        },

        RemoveDbValue: function (key) {

            removeDbValue(key);

        },

    };


})();

//#endregion;
