const csrf_token = $('meta[name="csrf-token"]').attr('content');
const csrfSafeMethod = method => {
    // these HTTP methods do not require CSRF protection
    return /^(GET|HEAD|OPTIONS)$/i.test(method);
};
$(document).ajaxSend(function(event, jqXHR, settings) {
    if (!csrfSafeMethod(settings.type) && !settings.crossDomain) {
        jqXHR.setRequestHeader("X-CSRF-Token", csrf_token);
    }
});

var p = {
    name: 'Unknown',
    version: 'Unknown',
    os: 'Unknown'
};
if (typeof platform !== 'undefined') {
    p.name = platform.name;
    p.version = platform.version;
    p.os = platform.os.toString();
}

if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
    if (!getCookie('browserDismiss')) {
        var $browser_warning = $('<div id="browser-warning">' +
            '<i class="fa fa-exclamation-circle"></i>&nbsp;' +
            'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
            'Please use a different browser such as Chrome or Firefox.' +
            '<button type="button" class="close"><i class="fa fa-remove"></i></button>' +
            '</div>');
        $('body').prepend($browser_warning);
        var offset = $browser_warning.height();
        warningOffset(offset);

        $browser_warning.one('click', 'button.close', function () {
            $browser_warning.remove();
            warningOffset(-offset);
            setCookie('browserDismiss', 'true', 7);
        });

        function warningOffset(offset) {
            var navbar = $('.navbar-fixed-top');
            if (navbar.length) {
                navbar.offset({top: navbar.offset().top + offset});
            }
            var container = $('.body-container');
            if (container.length) {
                container.offset({top: container.offset().top + offset});
            }
        }
    }
}

function initConfigCheckbox(elem, toggleElem, reverse) {
    toggleElem = (toggleElem === undefined) ? null : toggleElem;
    reverse = (reverse === undefined) ? false : reverse;
    var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
    config.addClass('hidden-settings');
    if ($(elem).is(":checked")) {
        config.toggle(!reverse);
    } else {
        config.toggle(reverse);
    }
    $(elem).click(function () {
        var config = toggleElem ? $(toggleElem) : $(this).closest('div').next();
        if ($(this).is(":checked")) {
            config.slideToggleBool(!reverse);
        } else {
            config.slideToggleBool(reverse);
        }
    });
}

function refreshTab() {
    var url = $(location).attr('href');
    var tabId = $('.ui-tabs-panel:visible').attr("id");
    $('.ui-tabs-panel:visible').load(url + " #" + tabId, function () {
        initThisPage();
    });
}

function showMsg(msg, loader, timeout, ms, error) {
    var feedback = $("#ajaxMsg");
    var update = $("#updatebar");
    var token_error = $("#token_error_bar");
    if (update.is(":visible") || token_error.is(":visible")) {
        var height = (update.is(":visible") ? update.height() : 0) + (token_error.is(":visible") ? token_error.height() : 0) + 35;
        feedback.css("bottom", height + "px");
    } else {
        feedback.removeAttr("style");
    }
    var message = $("<div class='msg'>" + msg + "</div>");
    if (loader) {
        message = $("<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbsp; " + msg + "</div>");
        feedback.css("padding", "14px 10px");
    }
    if (error) {
        feedback.css("background-color", "rgba(255,0,0,0.5)");
    }
    $(feedback).html(message);
    feedback.fadeIn();
    if (timeout) {
        setTimeout(function () {
            message.fadeOut(function () {
                $(this).remove();
                feedback.fadeOut();
                feedback.css("background-color", "");
            });
        }, ms);
    }
}

function confirmAjaxCall(url, msg, data, loader_msg, callback) {
    $("#confirm-message").html(msg);
    $('#confirm-modal').modal();
    $('#confirm-modal').off('click', '#confirm-button').one('click', '#confirm-button', function () {
        if (loader_msg) {
            showMsg(loader_msg, true, false);
        }
        $.ajax({
            url: url,
            type: 'POST',
            cache: false,
            async: true,
            data: data,
            complete: function (xhr, status) {
                var result = $.parseJSON(xhr.responseText);
                var msg = result.message;
                if (result.result == 'success') {
                    showMsg('<i class="fa fa-check"></i>&nbsp; ' + msg, false, true, 5000);
                } else {
                    showMsg('<i class="fa fa-times"></i>&nbsp; ' + msg, false, true, 5000, true);
                }
                if (typeof callback === "function") {
                    callback(result);
                }
            }
        });
    });
}

function doAjaxCall(url, elem, reload, form, showMsg, callback) {
    // Set Message
    var feedback = (showMsg) ? $("#ajaxMsg") : $();
    var update = $("#updatebar");
    var token_error = $("#token_error_bar");
    if (update.is(":visible") || token_error.is(":visible")) {
        var height = (update.is(":visible") ? update.height() : 0) + (token_error.is(":visible") ? token_error.height() : 0) + 35;
        feedback.css("bottom", height + "px");
    } else {
        feedback.removeAttr("style");
    }
    feedback.fadeIn();
    // Get Form data
    var formID = "#" + url;
    var dataString;
    if (form === true) {
        dataString = $(formID).serialize();
    }
    // Loader Image
    var loader = $("<div class='msg ajaxLoader-" + url +"'><i class='fa fa-refresh fa-spin'></i>&nbsp; Saving...</div>");
    // Data Success Message
    var dataSucces = $(elem).data('success');
    if (typeof dataSucces === "undefined") {
        // Standard Message when variable is not set
        dataSucces = "Success!";
    }
    // Data Error Message
    var dataError = $(elem).data('error');
    if (typeof dataError === "undefined") {
        // Standard Message when variable is not set
        dataError = "There was an error";
    }
    // Get Success & Error message from inline data, else use standard message
    var succesMsg = $("<div class='msg'><i class='fa fa-check'></i>&nbsp; " + dataSucces + "</div>");
    var errorMsg = $("<div class='msg'><i class='fa fa-exclamation-triangle'></i>&nbsp; " + dataError + "</div>");
    // Check if checkbox is selected
    if (form) {
        if ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ||
            $('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) {
            feedback.addClass('error');
            $(feedback).prepend(errorMsg);
            setTimeout(function () {
                errorMsg.fadeOut(function () {
                    $(this).remove();
                    feedback.fadeOut(function () {
                        feedback.removeClass('error');
                    });
                });
                $(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
            }, 2000);
            return false;
        }
    }
    // Ajax Call
    $.ajax({
        url: url,
        data: dataString,
        type: 'POST',
        beforeSend: function (jqXHR, settings) {
            // Start loader etc.
            feedback.prepend(loader);
        },
        error: function (jqXHR, textStatus, errorThrown) {
            feedback.addClass('error');
            feedback.prepend(errorMsg);
            setTimeout(function () {
                errorMsg.fadeOut(function () {
                    $(this).remove();
                    feedback.fadeOut(function () {
                        feedback.removeClass('error');
                    });
                });
            }, 2000);
        },
        success: function (data, jqXHR) {
            feedback.prepend(succesMsg);
            feedback.addClass('success');
            setTimeout(function (e) {
                succesMsg.fadeOut(function () {
                    $(this).remove();
                    feedback.fadeOut(function () {
                        feedback.removeClass('success');
                    });
                    if (reload === true) refreshSubmenu();
                    if (reload === "table") {
                        refreshTable();
                    }
                    if (reload === "tabs") refreshTab();
                    if (reload === "page") location.reload();
                    if (reload === "submenu&table") {
                        refreshSubmenu();
                        refreshTable();
                    }
                    if (form) {
                        // Change the option to 'choose...'
                        $(formID + " select").children('option[disabled=disabled]').attr(
                            'selected', 'selected');
                    }
                });
            }, 2000);
        },
        complete: function (jqXHR, textStatus) {
            // Remove loaders and stuff, ajax request is complete!
            $('.ajaxLoader-' + url).remove();
            if (typeof callback === "function") {
                callback(jqXHR);
            }
        }
    });
}

getBrowsePath = function (key, path, filter_ext) {
    var deferred = $.Deferred();

    $.ajax({
        url: 'browse_path',
        type: 'GET',
        data: {
            key: key,
            path: path,
            filter_ext: filter_ext
        },
        success: function(data) {
            deferred.resolve(data);
        },
        error: function() {
            deferred.reject();
        }
    });
    return deferred;
};

function doSimpleAjaxCall(url) {
    $.ajax(url);
}

function resetFilters(text) {
    if ($(".dataTables_filter").length > 0) {
        $(".dataTables_filter input").attr("placeholder", "filter " + text + "");
    }
}

function isPrivateIP(ip_address) {
    var defer = $.Deferred();

    if (ipaddr.isValid(ip_address)) {
        var addr = ipaddr.process(ip_address);
        if (addr.range() === 'loopback' || addr.range() === 'private' || addr.range() === 'linkLocal') {
            defer.resolve();
        } else {
            defer.reject();
        }
    } else {
        defer.resolve('n/a');
    }

    return defer.promise();
}

function humanTime(seconds) {
    if (seconds > 0) {
        return humanDuration(seconds * 1000).replaceAll(/(\d+) (\w+)/g, '<h3>$1</h3><p>$2</p>')
    }
    return "<h3>0</h3><p>mins</p>";
}

String.prototype.toProperCase = function () {
    return this.replace(/\w\S*/g, function (txt) {
        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
    });
};

function getPercent(value1, value2) {
    value1 = parseFloat(value1) | 0
    value2 = parseFloat(value2) | 0

    var percent = 0;
    if (value1 !== 0 && value2 !== 0) {
        percent = (value1 / value2) * 100
    }

    return Math.round(percent)
}

function millisecondsToHoursMinutes(ms) {
    if (ms > 0) {
        var hours = Math.floor(ms / 3600000);
        var minutes = Math.floor((ms % 3600000) / 60000);
        var seconds = Math.floor(((ms % 3600000) % 60000) / 1000);
        if (hours > 0) {
            return hours + ":" + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
        }
        return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
    } else {
        return '0:00';
    }
}

function humanDuration(ms, sig='dhm', units='ms', return_seconds=300000) {
    var factors = {
        d: 86400000,
        h: 3600000,
        m: 60000,
        s: 1000,
        ms: 1
    }

    ms = parseInt(ms);
    var d, h, m, s;

    if (ms > 0) {
        if (return_seconds && ms < return_seconds) {
            sig = 'dhms'
        }

        r = factors[sig.slice(-1)];
        ms = Math.round(ms * factors[units] / r) * r;

        h =  ms % factors['d'];
        d = Math.trunc(ms / factors['d']);

        m = h % factors['h'];
        h = Math.trunc(h / factors['h']);

        s = m % factors['m'];
        m = Math.trunc(m / factors['m']);

        ms = s % factors['s'];
        s = Math.trunc(s / factors['s']);

        var hd_list = [];
        if (sig >= 'd' && d > 0) {
            d = (sig === 'd' && h >= 12) ? d + 1 : d;
            hd_list.push(d.toString() + ' day' + ((d > 1) ? 's' : ''));
        }
        if (sig >= 'dh' && h > 0) {
            h = (sig === 'dh' && m >= 30) ? h + 1 : h;
            hd_list.push(h.toString() + ' hr' + ((h > 1) ? 's' : ''));
        }
        if (sig >= 'dhm' && m > 0) {
            m = (sig === 'dhm' && s >= 30) ? m + 1 : m;
            hd_list.push(m.toString() + ' min' + ((m > 1) ? 's' : ''));
        }
        if (sig >= 'dhms' && s > 0) {
            hd_list.push(s.toString() + ' sec' + ((s > 1) ? 's' : ''));
        }

        return hd_list.join(' ')
    } else {
        return '0'
    }
}

// Our countdown plugin takes a callback, a duration, and an optional message
$.fn.countdown = function (callback, duration, message) {
    // If no message is provided, we use an empty string
    message = message || "";
    // Get reference to container, and set initial content
    var container = $(this[0]).html(duration + message);
    // Get reference to the interval doing the countdown
    var countdown = setInterval(function () {
        // If seconds remain
        if (--duration) {
            // Update our container's message
            container.html(duration + message);
            // Otherwise
        } else {
            // Clear the countdown interval
            clearInterval(countdown);
            // And fire the callback passing our container as `this`
            callback.call(container);
        }
        // Run interval every 1000ms (1 second)
    }, 1000);
};

function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
    var expires = "expires=" + d.toUTCString();
    document.cookie = cname + "=" + cvalue + "; " + expires;
}

function getCookie(cname) {
    var name = cname + "=";
    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);
        if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
    }
    return "";
}
var Accordion = function (el, multiple, close) {
    this.el = el || {};
    this.multiple = multiple || false;
    this.close = (close === undefined) ? true : close;
    // Variables privadas
    var links = this.el.find('.link');
    // Evento
    links.on('click', {
        el: this.el,
        multiple: this.multiple,
        close: this.close
    }, this.dropdown);
};
Accordion.prototype.dropdown = function (e) {
    var $el = e.data.el;
    $this = $(this);
    $next = $this.next();
    if (!e.data.close && $this.parent().hasClass('open')) {
        return
    }
    $next.slideToggle();
    $this.parent().toggleClass('open');
    if (!e.data.multiple) {
        $el.find('.submenu').not($next).slideUp().parent().removeClass('open');
    }
};

function clearSearchButton(tableName, table) {
    $('#' + tableName + '_filter').find('input[type=search]').wrap(
        '<div class="input-group" role="group" aria-label="Search"></div>').after(
        '<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' +
        tableName + '"><i class="fa fa-remove"></i></button></span>');
    $('#clear-search-' + tableName).click(function () {
        table.search('').draw();
    });
}
// Taken from https://github.com/Hellowlol/HTPC-Manager
window.onerror = function (message, file, line) {
    var e = {
        'page': window.location.href,
        'message': message,
        'file': file,
        'line': line
    };
    $.post("log_js_errors", e, function (data) { });
};

$('*').on('click', '.refresh_pms_image', function (e) {
    e.preventDefault();
    e.stopPropagation();

    var background_div = $(this).parent().siblings(['style*=pms_image_proxy']).first();
    var pms_proxy_url = background_div.css('background-image');
    pms_proxy_url = /^url\((['"]?)(.*)\1\)$/.exec(pms_proxy_url);
    pms_proxy_url = pms_proxy_url ? pms_proxy_url[2] : ""; // If matched, retrieve url, otherwise ""

    if (pms_proxy_url.indexOf('pms_image_proxy') === -1) {
        console.log('PMS image proxy url not found.');
    } else {
        background_div.css('background-image', 'none')
        $.ajax({
            url: pms_proxy_url,
            headers: {
                'Cache-Control': 'no-cache'
            },
            success: function () {
                background_div.css('background-image', 'url(' + pms_proxy_url + ')');
            }
        });
    }
});

// Taken from http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable#answer-14919494
function humanFileSize(bytes, si = true) {
    //var thresh = si ? 1000 : 1024;
    var thresh = 1024;  // Always divide by 2^10 but display SI units
    if (Math.abs(bytes) < thresh) {
        return bytes + ' B';
    }
    var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
        : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
    var u = -1;
    do {
        bytes /= thresh;
        ++u;
    } while (Math.abs(bytes) >= thresh && u < units.length - 1);
    return bytes.toFixed(1) + '&nbsp;' + units[u];
}

// Force max/min in number inputs
function forceMinMax(elem) {
    var min = parseInt(elem.attr('min'));
    var max = parseInt(elem.attr('max'));
    var val = parseInt(elem.val());
    var default_val = parseInt(elem.data('default'));
    if (isNaN(val)) {
        elem.val(default_val);
    }
    else if (min !== undefined && val < min) {
        elem.val(min);
    }
    else if (max !== undefined && val > max) {
        elem.val(max);
    }
    else {
        elem.val(val);
    }
}

function capitalizeFirstLetter(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
}

$.fn.slideToggleBool = function(bool, options) {
  return bool ? $(this).slideDown(options) : $(this).slideUp(options);
};

function openPlexXML(endpoint, plextv, params) {
    var data = $.extend({endpoint: endpoint, plextv: plextv || false}, params);
    var query = new URLSearchParams(data)
    window.open('open_plex_xml?' + query.toString(), '_blank');
}

function PopupCenter(url, title, w, h) {
    // Fixes dual-screen position                         Most browsers      Firefox
    var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX;
    var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY;

    var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
    var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;

    var left = ((width / 2) - (w / 2)) + dualScreenLeft;
    var top = ((height / 2) - (h / 2)) + dualScreenTop;
    var newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);

    // Puts focus on the newWindow
    if (window.focus) {
        newWindow.focus();
    }

    return newWindow;
}


function setLocalStorage(key, value, path) {
    var key_path = key;
    if (path !== false) {
        key_path = key_path + '_' + window.location.pathname;
    }
    localStorage.setItem(key_path, value);
}
function getLocalStorage(key, default_value, path) {
    var key_path = key;
    if (path !== false) {
        key_path = key_path + '_' + window.location.pathname;
    }
    var value = localStorage.getItem(key_path);
    if (value !== null) {
        return value
    } else if (default_value !== undefined) {
        setLocalStorage(key, default_value, path);
        return default_value
    }
}

function uuidv4() {
    return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
        var cryptoObj = window.crypto || window.msCrypto; // for IE 11
        return (c ^ cryptoObj.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    });
}

function getPlexHeaders(clientID) {
    return {
        'Accept': 'application/json',
        'X-Plex-Product': 'Tautulli',
        'X-Plex-Version': 'Plex OAuth',
        'X-Plex-Client-Identifier': clientID ? clientID : getLocalStorage('Tautulli_ClientID', uuidv4(), false),
        'X-Plex-Platform': p.name,
        'X-Plex-Platform-Version': p.version,
        'X-Plex-Model': 'Plex OAuth',
        'X-Plex-Device': p.os,
        'X-Plex-Device-Name': p.name + ' (Tautulli)',
        'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height,
        'X-Plex-Language': 'en'
    };
}

var plex_oauth_window = null;
const plex_oauth_loader = '<style>' +
        '.login-loader-container {' +
            'font-family: "Open Sans", Arial, sans-serif;' +
            'position: absolute;' +
            'top: 0;' +
            'right: 0;' +
            'bottom: 0;' +
            'left: 0;' +
        '}' +
        '.login-loader-message {' +
            'color: #282A2D;' +
            'text-align: center;' +
            'position: absolute;' +
            'left: 50%;' +
            'top: 25%;' +
            'transform: translate(-50%, -50%);' +
        '}' +
        '.login-loader {' +
            'border: 5px solid #ccc;' +
            '-webkit-animation: spin 1s linear infinite;' +
            'animation: spin 1s linear infinite;' +
            'border-top: 5px solid #282A2D;' +
            'border-radius: 50%;' +
            'width: 50px;' +
            'height: 50px;' +
            'position: relative;' +
            'left: calc(50% - 25px);' +
        '}' +
        '@keyframes spin {' +
            '0% { transform: rotate(0deg); }' +
            '100% { transform: rotate(360deg); }' +
        '}' +
    '</style>' +
    '<div class="login-loader-container">' +
        '<div class="login-loader-message">' +
            '<div class="login-loader"></div>' +
            '<br>' +
            'Redirecting to the Plex login page...' +
        '</div>' +
    '</div>';

function closePlexOAuthWindow() {
    if (plex_oauth_window) {
        plex_oauth_window.close();
    }
}

getPlexOAuthPin = function (clientID) {
    var x_plex_headers = getPlexHeaders(clientID);
    var deferred = $.Deferred();

    $.ajax({
        url: 'https://plex.tv/api/v2/pins?strong=true',
        type: 'POST',
        headers: x_plex_headers,
        success: function(data) {
            deferred.resolve({pin: data.id, code: data.code});
        },
        error: function() {
            closePlexOAuthWindow();
            deferred.reject();
        }
    });
    return deferred;
};

var polling = null;

function PlexOAuth(successCallback, errorCallback, maxRetryCallback, pollingCallback, preFunction, clientID) {
    if (typeof preFunction === "function") {
        preFunction()
    }
    closePlexOAuthWindow();
    plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700);
    $(plex_oauth_window.document.body).html(plex_oauth_loader);

    getPlexOAuthPin(clientID).then(function (data) {
        var x_plex_headers = getPlexHeaders(clientID);
        const pin = data.pin;
        const code = data.code;

        var oauth_params = {
            'clientID': x_plex_headers['X-Plex-Client-Identifier'],
            'context[device][product]': x_plex_headers['X-Plex-Product'],
            'context[device][version]': x_plex_headers['X-Plex-Version'],
            'context[device][platform]': x_plex_headers['X-Plex-Platform'],
            'context[device][platformVersion]': x_plex_headers['X-Plex-Platform-Version'],
            'context[device][device]': x_plex_headers['X-Plex-Device'],
            'context[device][deviceName]': x_plex_headers['X-Plex-Device-Name'],
            'context[device][model]': x_plex_headers['X-Plex-Model'],
            'context[device][screenResolution]': x_plex_headers['X-Plex-Device-Screen-Resolution'],
            'context[device][layout]': 'desktop',
            'code': code
        }

        plex_oauth_window.location = 'https://app.plex.tv/auth/#!?' + encodeData(oauth_params);
        polling = pin;  // Set pin to prevent polling from multiple popups

        let maxPollCount = 120;  // 2 minutes at 1-second intervals

        (function poll() {
            maxPollCount--;

            $.ajax({
                url: 'https://plex.tv/api/v2/pins/' + pin,
                type: 'GET',
                headers: x_plex_headers,
                success: function (data) {
                    if (data.authToken){
                        polling = null;
                        closePlexOAuthWindow();
                        if (typeof successCallback === "function") {
                            successCallback(data.authToken)
                        }
                    }
                },
                complete: function () {
                    if (maxPollCount <= 0) {
                        closePlexOAuthWindow();
                        if (typeof maxRetryCallback === "function") {
                            maxRetryCallback()
                        }
                    } else if (polling === pin) {
                        setTimeout(function() {poll()}, 1000);
                        if (typeof pollingCallback === "function") {
                            pollingCallback(maxPollCount);
                        }
                    }
                },
                timeout: 1000
            });
        })();
    }, function () {
        closePlexOAuthWindow();
        if (typeof errorCallback === "function") {
            errorCallback()
        }
    });
}

function encodeData(data) {
    return Object.keys(data).map(function(key) {
        return [key, data[key]].map(encodeURIComponent).join("=");
    }).join("&");
}

function page(endpoint, ...args) {
    let endpoints = {
        'pms_image_proxy': pms_image_proxy,
        'info': info_page,
        'library': library_page,
        'user': user_page
    };

    var params = {};

    if (endpoint in endpoints) {
        params = endpoints[endpoint](...args);
    }

    return endpoint + '?' + $.param(params).replace(/'/g, '%27');
}

function pms_image_proxy(img, rating_key, width, height, opacity, background, blur, fallback, refresh, clip, img_format) {
    var params = {};

    if (img != null) { params.img = img; }
    if (rating_key != null) { params.rating_key = rating_key; }
    if (width != null) { params.width = width; }
    if (height != null) { params.height = height; }
    if (opacity != null) { params.opacity = opacity; }
    if (background != null) { params.background = background; }
    if (blur != null) { params.blur = blur; }
    if (fallback != null) { params.fallback = fallback; }
    if (refresh != null) { params.refresh = true; }
    if (clip != null) { params.clip = true; }
    if (img_format != null) { params.img_format = img_format; }

    return params;
}

function info_page(rating_key, guid, history, live) {
    var params = {};

    if (live && history) {
        params.guid = guid;
    } else {
        params.rating_key = rating_key;
    }

    if (history) { params.source = 'history'; }

    return params;
}

function library_page(section_id) {
    var params = {};

    if (section_id != null) { params.section_id = section_id; }

    return params;
}

function user_page(user_id, user) {
    var params = {};

    if (user_id != null) { params.user_id = user_id; }
    if (user != null) { params.user = user; }

    return params;
}

MEDIA_TYPE_HEADERS = {
    'movie': 'Movies',
    'show': 'TV Shows',
    'season': 'Seasons',
    'episode': 'Episodes',
    'artist': 'Artists',
    'album': 'Albums',
    'track': 'Tracks',
    'video': 'Videos',
    'audio': 'Tracks',
    'photo': 'Photos'
}

function short_season(title) {
    if (title.startsWith('Season ') && /^\d+$/.test(title.substring(7))) {
        return 'S' + title.substring(7)
    }
    return title
}

function loadAllBlurHash() {
    $('[data-blurhash]').each(function() {
        const elem = $(this);
        const src = elem.data('blurhash');
        loadBlurHash(elem, src);
    });
}

function loadBlurHash(elem, src) {
    const img = new Image();
    img.src = src;
    img.onload = () => {
        const imgData = blurhash.getImageData(img);

        blurhash
            .encodePromise(imgData, img.width, img.height, 4, 4)
            .then(hash => {
                return blurhash.decodePromise(
                    hash,
                    img.width,
                    img.height
                );
            })
            .then(blurhashImgData => {
                const imgObject = blurhash.getImageDataAsImage(
                    blurhashImgData,
                    img.width,
                    img.height,
                    (event, imgObject) => {
                        elem.css('background-image', 'url(' + imgObject.src + ')')
                    }
                );
            });
    }
}

function _toggleRevealToken(elem, click) {
    var input = elem.parent().siblings('input');
    if ((input.prop('type') === 'password' && click) || !input.val()) {
        input.prop('type', 'text');
        elem.children('.fa').removeClass('fa-eye-slash').addClass('fa-eye');
    } else {
        input.prop('type', 'password');
        elem.children('.fa').removeClass('fa-eye').addClass('fa-eye-slash');
    }
}
function toggleRevealTokens() {
    $('.reveal-token').each(function () {
        _toggleRevealToken($(this));
    });
}

$('body').on('click', '.reveal-token', function() {
    _toggleRevealToken($(this), true);
});

// https://stackoverflow.com/a/57414592
// prevent modal close when click starts in modal and ends on backdrop
$(document).on('mousedown', '.modal', function(e){
    window.clickStartedInModal = $(e.target).is('.modal-dialog *');
});
$(document).on('mouseup', '.modal', function(e){
    if(!$(e.target).is('.modal-dialog *') && window.clickStartedInModal) {
        window.preventModalClose = true;
    }
});
$('.modal').on('hide.bs.modal', function (e) {
    if(window.preventModalClose){
        window.preventModalClose = false;
        return false;
    }
});

$.fn.hasScrollBar = function() {
    return this.get(0).scrollHeight > this.get(0).clientHeight;
}

function paginateScroller(scrollerId, buttonClass) {
    $(buttonClass).click(function (e) {
        e.preventDefault();
        var scroller = $(scrollerId + "-row-scroller");
        var scrollerParent = scroller.parent();
        var containerWidth = scrollerParent.width();
        var scrollCurrent = scrollerParent.scrollLeft();
        var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
        var scrollMax = scroller.width() - Math.abs(scrollAmount);
        var scrollTotal = Math.min(parseInt(scrollCurrent / 175) * 175 + scrollAmount, scrollMax);
        scrollerParent.animate({ scrollLeft: scrollTotal }, 250);
    });    
}

function highlightScrollerButton(scrollerId) {
    var scroller = $(scrollerId + "-row-scroller");
    var scrollerParent = scroller.parent();
    var buttonLeft = $(scrollerId + "-page-left");
    var buttonRight = $(scrollerId + "-page-right");

    var numElems = scroller.find("li").length;
    scroller.width(numElems * 175);
    $(buttonLeft).addClass("disabled").blur();
    if (scroller.width() > scrollerParent.width()) {
        $(buttonRight).removeClass("disabled");
    } else {
        $(buttonRight).addClass("disabled");
    }

    scrollerParent.scroll(function () {
        var scrollCurrent = $(this).scrollLeft();
        var scrollMax = scroller.width() - $(this).width();

        if (scrollCurrent == 0) {
            $(buttonLeft).addClass("disabled").blur();
        } else {
            $(buttonLeft).removeClass("disabled");
        }

        if (scrollCurrent >= scrollMax) {
            $(buttonRight).addClass("disabled").blur();
        } else {
            $(buttonRight).removeClass("disabled");
        }
    });
}
