<% import plexpy from plexpy import version from plexpy.helpers import anon_url from plexpy.notifiers import BROWSER_NOTIFIERS %> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Tautulli - ${title} | ${server_name}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> <meta name="referrer" content="no-referrer"> <link href="${http_root}css/bootstrap3/bootstrap.min.css" rel="stylesheet"> <link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" /> <link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" /> <link href="${http_root}css/selectize.min.css" rel="stylesheet" /> <link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet"> <link href="${http_root}css/opensans.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet"> ${next.headIncludes()} <!-- Favicons --> <link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0"> <link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0"> <link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0"> <!-- ICONS --> <!-- Android --> <link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.9.0" crossorigin="use-credentials"> <meta name="theme-color" content="#282a2d"> <!-- Apple --> <link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0"> <link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d"> <meta name="apple-mobile-web-app-title" content="Tautulli"> <!-- Microsoft --> <meta name="application-name" content="Tautulli"> <meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0"> </head> <body class="content"> <div class="container"> <div id="ajaxMsg" class="ajaxMsg"></div> % if _session['user_group'] == 'admin': % if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is not False: <div id="updatebar" style="display: none;"> % if plexpy.UPDATE_AVAILABLE is None: You are running an unknown version of Tautulli.<br /> % elif plexpy.UPDATE_AVAILABLE == 'release': A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer"> new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br /> % elif plexpy.UPDATE_AVAILABLE == 'commit': A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank" rel="noreferrer"> newer version</a> of Tautulli is available!<br /> You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br /> % endif % if plexpy.INSTALL_TYPE == 'docker': Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a> % elif plexpy.INSTALL_TYPE == 'snap': Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a> % elif plexpy.INSTALL_TYPE in ('windows', 'macos'): <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a> % else: <a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> % endif </div> % else: <div id="updatebar" style="display: none;"></div> % endif % endif <nav class="navbar navbar-fixed-top"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="home" title="Tautulli"> <img src="${http_root}images/logo-tautulli-45.png" height="45" alt="Tautulli"> </a> </div> <div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1"> <ul class="nav navbar-nav"> <li> <form action="search" method="post" class="form" id="search_form"> <div class="input-group"> <span class="input-textbox"> <input type="text" class="form-control" name="query" id="query" aria-label="Search" placeholder="Search Plex library..."/> </span> <span class="input-group-btn"> <button class="btn btn-dark btn-inactive" type="submit" id="search_button"><i class="fa fa-search"></i></button> </span> </div> </form> </li> % if title == "Home": <li class="active"><a href="home"><i class="fa fa-lg fa-home"></i></a></li> % else: <li><a href="home"><i class="fa fa-lg fa-home"></i></a></li> % endif % if title == "Libraries" or title == "Library" or title == "Info": <li class="active"><a href="libraries">Libraries</a></li> % else: <li><a href="libraries">Libraries</a></li> % endif % if title == "Users" or title == "User": <li class="active"><a href="users">Users</a></li> % else: <li><a href="users">Users</a></li> % endif % if title == "History": <li class="active"><a href="history">History</a></li> % else: <li><a href="history">History</a></li> % endif % if title == "Graphs": <li class="active"><a href="graphs">Graphs</a></li> % else: <li><a href="graphs">Graphs</a></li> % endif % if title == "Settings": <li class="dropdown active"> % else: <li class="dropdown"> % endif <% href = 'settings' if _session['user_group'] == 'admin' else '#' %> <a href="#" class="dropdown-toggle" aria-haspopup="true" data-toggle="dropdown" data-hover="dropdown" data-href="${href}"><i class="fa fa-lg fa-cogs"></i> <span class="caret"></span></a> <ul class="dropdown-menu" id="settings-dropdown-menu"> % if _session['user_group'] == 'admin': <li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li> <li role="separator" class="divider"></li> <li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li> <li><a href="${anon_url('https://github.com/%s/%s/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank" rel="noreferrer"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li> <li><a href="support"><i class="fa fa-fw fa-comment"></i> Support</a></li> <li role="separator" class="divider"></li> <li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li> <li role="separator" class="divider"></li> % if plexpy.CONFIG.CHECK_GITHUB: <li><a href="#" id="nav-update"><i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates</a></li> % endif <li><a href="#" id="nav-restart"><i class="fa fa-fw fa-refresh"></i> Restart</a></li> <li><a href="#" id="nav-shutdown"><i class="fa fa-fw fa-power-off"></i> Shutdown</a></li> % else: <li><a href="#" data-target="#admin-login-modal" data-toggle="modal"><i class="fa fa-fw fa-lock"></i> Admin Login</a></li> <li role="separator" class="divider"></li> % endif % if _session['exp']: <li><a href="${http_root}auth/logout"><i class="fa fa-fw fa-sign-out"></i> Sign Out</a></li> % endif </ul> </li> </ul> </div> </div> </nav> </div> ${next.headerIncludes()} <div class="body-container"> ${next.body()} </div> ${next.modalIncludes()} % if _session['user_group'] != 'admin': <div id="admin-login-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="admin-login-modal"> <div class="modal-dialog" role="document"> <div class="modal-content"> <form id="login-form"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button> <h4 class="modal-title">Admin Login</h4> </div> <div class="modal-body"> <div class="col-md-6" style="margin: auto;"> <div class="form-group"> <label for="username" class="control-label"> Username </label> <input type="text" id="username" name="username" class="form-control" autocorrect="off" autocapitalize="off"> </div> <div class="form-group"> <label for="password" class="control-label"> Password </label> <input type="password" id="password" name="password" class="form-control"> </div> <div class="form-footer"> <div class="remember-group"> <label class="control-label"> <input type="checkbox" id="remember_me" name="remember_me" title="for 30 days" value="1" checked="checked" /> Remember me </label> </div> </div> </div> </div> <div class="modal-footer"> <span id="sign-in-alert" style="padding-right: 25px; display: none;"></span> <button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button> </div> <input type="hidden" id="admin_login" name="admin_login" value="1" /> </form> </div> </div> </div> % else: <div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="donate-modal"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button> <h4 class="modal-title">Tautulli Donation</h4> </div> <div class="modal-body"> <div class="row"> <div class="col-md-12" style="text-align: center;"> <h4> <strong>Thank you for supporting Tautulli!</strong> </h4> <p> Please select a donation method. </p> </div> </div> <ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;"> <li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li> <li><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li> <li><a href="#stripe-donation" role="tab" data-toggle="tab">Stripe</a></li> <li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li> <li><a href="#crypto-donation" role="tab" data-toggle="tab" id="crypto-donation-tab">Crypto</a></li> </ul> <div class="tab-content"> <div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center"> <p> Click the button below to continue to GitHub. </p> <p> <a href="${anon_url('https://github.com/sponsors/JonnyWong16')}" target="_blank" rel="noreferrer" class="btn btn-sm btn-default" style="font-weight: 600;"> <i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i> Sponsor </a> </p> <p class="small-muted">(GitHub does not have a fee)</p> </div> <div role="tabpanel" class="tab-pane" id="patreon-donation" style="text-align: center"> <p> Click the button below to continue to Patreon. </p> <p> <a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank" rel="noreferrer"> <img src="images/become_a_patron_button.png" alt="Become a Patron" width="170" height="40"> </a> </p> <p class="small-muted">(Patreon has a fee)</p> </div> <div role="tabpanel" class="tab-pane" id="stripe-donation" style="text-align: center"> <p> Click the button below to continue to Stripe. </p> <p> <a href="${anon_url('https://donate.stripe.com/5kA7vnb7dczVbxC9AA')}" target="_blank" rel="noreferrer"> <img src="images/Stripe_wordmark_-_white_small_28px.png" alt="Stripe" style="background-color: #7068fe; border-radius: 3px; padding: 3px;"> </a> </p> <p class="small-muted">(Stripe has a fee)</p> </div> <div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center"> <p> Click the button below to continue to PayPal. </p> <p> <a href="${anon_url('https://www.paypal.com/donate/?hosted_button_id=CUHSQ99KAKC5Q')}" target="_blank" rel="noreferrer"> <img src="images/gold-rect-paypal-34px.png" alt="PayPal"> </a> </p> <p class="small-muted">(PayPal has a fee)</p> </div> <div role="tabpanel" class="tab-pane" id="crypto-donation" style="text-align: center"> <p> Select a cryptocurrency. </p> <select class="form-control" id="crypto-select"></select> <div id="crypto-qrcode"></div> <div id="crypto-address" class="form-group"> <label>Address:</label> <span class="inline-pre" id="crypto-address-value"></span> </div> <p> Or click the button below to continue to Coinbase. </p> <a href="${anon_url('https://commerce.coinbase.com/checkout/8a9fa08c-8a38-409e-9220-868124c4ba0c')}" target="_blank" rel="noreferrer" class="donate-with-crypto"> <span>Donate with Crypto</span> </a> </div> </div> </div> <div class="modal-footer"> <input type="button" class="btn btn-bright" data-dismiss="modal" value="Close"> </div> </div> </div> </div> % endif <div id="confirm-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="confirm-modal"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button> <h4 class="modal-title">Confirm</h4> </div> <div class="modal-body"> <div id="confirm-message" style="text-align: center; margin-top: 20px; margin-bottom: 20px;"> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-button">Confirm</button> </div> </div> </div> </div> <script src="${http_root}js/jquery-3.6.0.min.js"></script> <script src="${http_root}js/bootstrap.min.js"></script> <script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script> <script src="${http_root}js/moment-with-locales.min.js"></script> <script src="${http_root}js/moment-duration-format.min.js"></script> <script src="${http_root}js/pnotify.custom.min.js"></script> <script src="${http_root}js/platform.min.js"></script> <script src="${http_root}js/ipaddr.min.js"></script> <script src="${http_root}js/selectize.min.js"></script> <script src="${http_root}js/jquery.tripleclick.min.js"></script> <script src="${http_root}js/blurhash_pure_js_port.min.js"></script> <script src="${http_root}js/script.js${cache_param}"></script> <script src="${http_root}js/ajaxNotifications.js"></script> <script src="${http_root}js/kjua.min.js"></script> <script> % if _session['user_group'] == 'admin': $('body').on('click', '#updateDismiss', function() { $('#updatebar').fadeOut(); // Set cookie to remember dismiss decision for 1 hour. setCookie('updateDismiss', 'true', 1/24); }); if (!getCookie('updateDismiss')) { if ($('#updatebar').html().length > 0) { $('#updatebar').show(); } } function checkUpdate(_callback) { // Allow the update bar to show again if previously dismissed. setCookie('updateDismiss', 'true', 0); $.ajax({ url: 'update_check', complete: function (xhr, status) { var result = $.parseJSON(xhr.responseText); var msg = ''; if (result.update === false) { showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000); } else { if (result.update === null) { msg = 'You are running an unknown version of Tautulli.<br />'; } else if (result.update === true && result.release === true) { msg = 'A <a href="' + result.release_url + '" target="_blank" rel="noreferrer">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />'; } else if (result.update === true && result.release === false) { msg = 'A <a href="' + result.compare_url + '" target="_blank" rel="noreferrer">newer version</a> of Tautulli is available!<br />' + 'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />'; } if (result.install_type === 'docker') { msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>'; } else if (result.install_type === 'snap') { msg += 'Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>'; } else if (result.install_type === 'windows' || result.install_type === 'macos') { msg += '<a href="' + result.release_url + '" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>' } else { msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>'; } $('#updatebar').html(msg).fadeIn(); } if (_callback) { _callback(); } } }); } $("#nav-shutdown").click(function() { $("#confirm-message").text("Are you sure you want to shutdown Tautulli?"); $('#confirm-modal').modal(); $('#confirm-modal').one('click', '#confirm-button', function () { window.location.href = "shutdown"; }); }); $("#nav-restart").click(function() { $("#confirm-message").text("Are you sure you want to restart Tautulli?"); $('#confirm-modal').modal(); $('#confirm-modal').one('click', '#confirm-button', function () { window.location.href = "restart"; }); }); $('#nav-update').click(function () { $(this).html('<i class="fa fa-fw fa-spin fa-refresh"></i> Checking'); checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); }); }); $('#crypto-donation-tab').one('shown.bs.tab', function (e) { $.ajax({ url: 'https://tautulli.com/donate/crypto-addresses.json', type: 'GET', dataType: 'json', cache: false, async: true, success: function (data) { $('#crypto-select').empty().append('<option selected disabled>Select Cryptocurrency</option>'); $.each(data, function (index, crypto) { $('<option/>', { text: crypto.name + ' (' + crypto.symbol + ')', value: crypto.address }).appendTo('#crypto-select'); }); }, error: function () { $('#crypto-select').empty().append('<option selected disabled>Error: Unable to load addresses</option>'); } }); }); $('#crypto-select').change(function() { var address = $(this).val(); $('#crypto-qrcode').empty().kjua({ text: address, render: 'canvas', ecLevel: 'H', size: 256, fill: '#000', back: '#eee' }).show(); $('#crypto-address-value').text(address); $('#crypto-address').show(); }) % endif $('.dropdown-toggle').click(function (e) { if (!(('ontouchstart' in window) || (navigator.MaxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0))) { window.location.href = $(this).data('href'); } }); function displaySearch() { if ($(this).width() < 768) { $('#search_button').removeClass('btn-inactive'); $('#query').css({ right: 0, width: '100%' }) } else if ($('#query').val().trim() == '') { $('#search_button').addClass('btn-inactive'); $('#query').css({ right: '-200px', width: '0' }) } } displaySearch(); $(window).resize(function() { displaySearch(); }); $('#search_form').submit(function (e) { if ($('#query').val().trim() != '') { $.ajax({ type: 'post', url: 'search', data: { query: $('#query').val() } }) } else { e.preventDefault(); if ($(window).width() >= 768) { $('#search_button').removeClass('btn-inactive'); $('#query').clearQueue().val('').animate({ right: '0', width: '200px' }).addClass('active').focus(); } } }) $('#query').on('blur', function (e) { if ($(this).val().trim() == '' && $(window).width() >= 768) { $(this).delay(200).animate({ right: '-200px', width: '0' }, function () { displaySearch(); }).removeClass('active'); } }); $(document).ready(function () { // Work around for iOS web app links opening in Safari if (("standalone" in window.navigator) && window.navigator.standalone) { // For iOS Apps $('a').on('click', function (e) { e.preventDefault(); var new_location = $(this).attr('href'); if (new_location != undefined && new_location.substr(0, 1) != '#' && $(this).attr('data-method') == undefined) { window.location = new_location; } }); } // Allow stacked bootstrap modals $(document).on('show.bs.modal', '.modal', function (event) { var zIndex = 1040 + (10 * $('.modal:visible').length); $(this).css('z-index', zIndex); setTimeout(function() { $('.modal-backdrop').not('.modal-stack').css('z-index', zIndex - 1).addClass('modal-stack'); }, 0); }); $(document).on('hidden.bs.modal', '.modal', function () { $('.modal:visible').length && $(document.body).addClass('modal-open'); }); % if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS: check_notifications(); % endif }); % if _session['user_group'] != 'admin': $('#admin-login-modal').on('shown.bs.modal', function () { $('#admin-login-modal #username').focus() }) $('#login-form').submit(function(event) { event.preventDefault(); $('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i> Sign In'); $.ajax({ url: '${http_root}auth/signin', type: 'POST', data: $(this).serialize(), dataType: 'json', statusCode: { 200: function(xhr, status) { window.location = "${http_root}"; }, 401: function(xhr, status) { $('#sign-in-alert').text('Incorrect username or password.').show(); $('#username').focus(); }, 429: function(xhr, status) { var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60) $('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show(); } }, complete: function() { $('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i> Sign In'); } }); }); % endif </script> ${next.javascriptIncludes()} </body> </html> <%def name="modalIncludes()"></%def> <%def name="javascriptIncludes()"></%def> <%def name="headIncludes()"></%def> <%def name="headerIncludes()"></%def>