<%inherit file="base.html"/> <%def name="headIncludes()"> <link href="${http_root}css/jquery.scrollbar.css" rel="stylesheet"> </%def> <%def name="body()"> <% from plexpy import PLEX_SERVER_UP %> <div class="container-fluid"> % for section in config['home_sections']: % if section == 'current_activity': <div class="row"> <div class="col-md-12"> <div class="padded-header" id="current-activity-header"> <h3><span id="sessions-xml">Activity</span> <small> <span id="currentActivityHeader" style="display: none;"> Sessions: <span id="currentActivityHeader-streams"></span> | Bandwidth: <span id="currentActivityHeader-bandwidth"></span> <span id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Reserved Bandwidth)"><i class="fa fa-info-circle"></i></span> </span> </small> </h3> </div> <div id="currentActivity"> % if PLEX_SERVER_UP: <div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div> % elif config['pms_is_cloud']: <div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div> % elif not config['first_run_complete']: <div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div> % else: <div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to the Plex server...</div> % endif </div> </div> </div> % elif section == 'watch_stats': <div class="row"> <div class="col-md-12"> <div class="home-padded-header padded-header"> <h3 class="pull-left">Watch Statistics</h3> <div class="button-bar"> <div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px"> <label class="btn btn-dark btn-filter"> <input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="plays" autocomplete="off"> Play Count </label> <label class="btn btn-dark btn-filter"> <input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="duration" autocomplete="off"> Play Duration </label> </div> <div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection"> <span class="input-group-addon btn-dark inactive">Last</span> <input type="number" class="form-control number-input" name="watched-stats-days" id="watched-stats-days" value="30" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" /> <span class="input-group-addon btn-dark inactive">days</span> </div> </div> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div id="home-stats" class="home-platforms"> <div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div> <br> </div> </div> </div> % elif section == 'library_stats': <div class="row"> <div class="col-md-12"> <div class="home-padded-header padded-header" id="library-statistics-header"> <h3 class="pull-left">Library Statistics</h3> <div class="button-bar"> <span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span> </div> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div id="library-stats" class="library-platforms"> <div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div> <br> </div> </div> </div> % elif section == 'recently_added': <div class="row"> <div class="col-md-12"> <div class="home-padded-header padded-header"> <h3 class="pull-left"><span id="recently-added-xml">Recently Added</span></h3> <ul class="nav nav-header nav-dashboard pull-right" style="margin-top: -3px;"> <li> <a href="#" id="recently-added-page-left" class="paginate-added btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-left"></i></a> </li> <li> <a href="#" id="recently-added-page-right" class="paginate-added btn-gray disabled" data-id="+1"><i class="fa fa-lg fa-chevron-right"></i></a> </li> </ul> <div class="button-bar"> <div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px"> <label class="btn btn-dark btn-filter" id="recently-added-label-all"> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="all" autocomplete="off"> All </label> <label class="btn btn-dark btn-filter" id="recently-added-label-movies"> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies </label> <label class="btn btn-dark btn-filter" id="recently-added-label-tv"> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows </label> <label class="btn btn-dark btn-filter" id="recently-added-label-music"> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-artist" value="artist" autocomplete="off"> Music </label> <label class="btn btn-dark btn-filter" id="recently-added-label-other_video"> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-other_video" value="other_video" autocomplete="off"> Videos </label> </div> <div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection"> <input type="number" class="form-control number-input" name="recently-added-count" id="recently-added-count" value="50" min="1" max="50" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 50 items" /> <span class="input-group-addon btn-dark inactive">items</span> </div> </div> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div id="recentlyAdded" style="margin-right: -15px;"> % if PLEX_SERVER_UP: <div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div> % elif config['pms_is_cloud']: <div class="text-muted">Plex Cloud server is sleeping.</div> % else: <div id="dashboard-no-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to your Plex server...</div> % endif </div> </div> </div> % endif % endfor </div> </%def> <%def name="modalIncludes()"> % if _session['user_group'] == 'admin' and config['update_show_changelog']: <% from plexpy.common import RELEASE %> <div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-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 Updated to <strong>${RELEASE}</strong></h4> </div> <div class="modal-body"> </div> <div class="modal-footer"> <button type="button" class="btn btn-danger" data-target="#donate-modal" data-toggle="modal" style="float: left;"><i class="fa fa-fw fa-heart"></i> Donate</button> <input type="button" class="btn btn-bright" data-dismiss="modal" value="Close"> </div> </div> </div> </div> % endif % if _session['user_group'] == 'admin': <div id="terminate-session-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="terminate-session-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">Terminate Stream</h4> </div> <div class="modal-body" style="text-align: center;"> <p>Are you sure you want to terminate this stream?</p> <p> <strong> <span id="terminate-user"></span><br /> <span id="terminate-title"></span><br /> <span id="terminate-subtitle"></span> </strong> </p> <br /> <label for="terminate-message">Terminate Message</label> <div class="row"> <div class="col-md-8" style="float: none; margin: 0 auto;"> <input type="text" class="form-control" id="terminate-message" name="terminate-message" value="" placeholder="The server owner has ended the stream." style="text-align: center;"> </div> </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-terminate-session">Terminate</button> </div> </div> </div> </div> <div class="modal fade wide" id="raw-stream-info-modal" tabindex="-1" role="dialog" aria-labelledby="raw-stream-info-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">Raw Stream Info: <strong><span id="raw-stream-info-title"></span> (<span id="raw-stream-info-user"></span>)</strong></h4> </div> <div class="modal-body"> <pre id="raw-stream-info"></pre> </div> <div class="modal-footer"> <input type="button" class="btn btn-bright" data-dismiss="modal" value="Close"> </div> </div> </div> </div> % endif <div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal"> </div> </%def> <%def name="javascriptIncludes()"> <script src="${http_root}js/jquery.scrollbar.min.js"></script> <script src="${http_root}js/jquery.mousewheel.min.js"></script> <script> var date_format = 'YYYY-MM-DD'; var time_format = 'hh:mm a'; $.ajax({ url: 'get_date_formats', type: 'GET', success: function (data) { date_format = data.date_format; time_format = data.time_format; } }); function lockScroll(elem) { $(elem).each(function (i, instance) { var childHeight = $(instance).children('.scoller-content').height(); var height = $(instance).height(); var scrollHeight = $(instance).get(0).scrollHeight; if (childHeight > height) { $(instance).on('mousewheel', function(e, d) { if(($(instance).scrollTop() >= (scrollHeight - height)) && d < 0 || ($(instance).scrollTop() <= 0) && d > 0) { e.preventDefault(); } }); } }); } % if _session['user_group'] == 'admin': var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.'; % else: var msg_settings = ''; % endif var error_msg = 'There was an error communicating with your Plex Server.' + msg_settings; % if 'current_activity' in config['home_sections'] or 'recently_added' in config['home_sections']: var server_status; server_status = setInterval(function() { $.getJSON('server_status', function (data) { if (data.connected === true) { clearInterval(server_status); % if 'current_activity' in config['home_sections']: $('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>'); activityConnected(); % endif % if 'recently_added' in config['home_sections']: $('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>'); recentlyAddedConnected(); % endif } else if (data.connected === false) { clearInterval(server_status); % if 'current_activity' in config['home_sections']: $('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>'); % endif % if 'recently_added' in config['home_sections']: $('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>'); % endif } }); }, 1000); % endif </script> % if 'current_activity' in config['home_sections']: <script> var defaultHandler = { get: function(target, name) { return target.hasOwnProperty(name) ? target[name] : 'Unknown'; } }; var create_instances = []; var activity_ready = true; $('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 }); function getCurrentActivity() { activity_ready = false; $.ajax({ url: 'get_activity', type: 'GET', cache: false, async: true, error: function (xhr, status, error) { console.log(status + ': ' + error); activity_ready = true; }, complete: function (xhr, status) { $('#dashboard-checking-activity').remove(); var current_activity; try { current_activity = $.parseJSON(xhr.responseText); } catch (e) { console.log(status + ': ' + e); current_activity = null; } if (!(current_activity)) { $('#currentActivityHeader').hide(); $('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>'); return } var stream_count = parseInt(current_activity.stream_count); var sessions = current_activity.sessions; if (stream_count) { $('#dashboard-no-activity').remove(); // Update the header stream counts var sc_dp = current_activity.stream_count_direct_play, sc_ds = current_activity.stream_count_direct_stream, sc_tc = current_activity.stream_count_transcode, total_bw = current_activity.total_bandwidth, lan_bw = current_activity.lan_bandwidth, wan_bw = current_activity.wan_bandwidth; var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' ('; if (sc_dp) { streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', '; } if (sc_ds) { streams_header += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', '; } if (sc_tc) { streams_header += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', '; } streams_header = streams_header.replace(/, $/, '') + ')'; $('#currentActivityHeader-streams').text(streams_header); var bandwidth_header = ((total_bw > 1000000) ? ((total_bw / 1000000).toFixed(1) + ' Gbps') : ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps'))); var lan_wan_bandwidth_header = ''; if (lan_bw) { lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000000) ? ((lan_bw / 1000000).toFixed(1) + ' Gbps') : ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps'))) + ', '; } if (wan_bw) { lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000000) ? ((wan_bw / 1000000).toFixed(1) + ' Gbps') : ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps'))) + ', '; } if (lan_wan_bandwidth_header) { bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')'; } $('#currentActivityHeader-bandwidth').text(bandwidth_header); $('#currentActivityHeader').show(); sessions.forEach(function (session) { var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session; var key = s.session_key; var session_id = s.session_id; var instance = $('#activity-instance-' + key); // Create a new instance if it doesn't exist or recreate the entire instance // if the rating key changed (for movies or episodes) of guid changed (for live tv) with the same session key if (!(instance.length) || (s.media_type !== 'track' && s.rating_key !== instance.data('rating_key').toString()) || (s.live === 1 && s.guid !== instance.data('guid'))) { create_instances.push(key); getActivityInstance(key); return; } // Update play state icon var state_icon = ''; switch (s.state) { case 'playing': state_icon = '<i class="fa fa-fw fa-play"></i> '; break; case 'paused': state_icon = '<i class="fa fa-fw fa-pause"></i> '; break; case 'buffering': state_icon = '<i class="fa fa-fw fa-spinner"></i> '; break; case 'error': state_icon = '<i class="fa fa-fw fa-exclamation-triangle"></i> '; break; default: state_icon = '<i class="fa fa-fw fa-question-circle"></i> '; } $('#play-state-' + key).html(state_icon).attr('title', capitalizeFirstLetter(s.state)); // Switching tracks can be under the same session key, so need to update the info. if (s.media_type === 'track') { // Update if artist changed if (s.grandparent_rating_key !== instance.data('grandparent_rating_key').toString()) { $('#background-' + key).css('background-image', 'url(' + page('pms_image_proxy', s.art, s.rating_key, 500, 280, 40, '282828', 3, 'art', true) + ')'); $('#metadata-grandparent_title-' + key) .attr('href', page('info', s.grandparent_rating_key)) .attr('title', s.original_title || s.grandparent_title) .text(s.original_title || s.grandparent_title); } // Update cover if album changed if (s.parent_rating_key !== instance.data('parent_rating_key').toString()) { $('#poster-' + key).css('background-image', 'url(' + page('pms_image_proxy', s.parent_thumb, s.parent_rating_key, 300, 300, null, null, null, 'poster', true) + ')'); $('#poster-' + key + '-bg').css('background-image', 'url(' + page('pms_image_proxy', s.parent_thumb, s.parent_rating_key, 300, 300, 60, '282828', 3, 'poster', true) + ')'); $('#poster-url-' + key) .attr('href', page('info', s.parent_rating_key)) .attr('title', s.parent_title); $('#metadata-parent_title-' + key) .attr('href', page('info', s.parent_rating_key)) .attr('title', s.parent_title) .text(s.parent_title); } // Update cover if track changed if (s.rating_key !== instance.data('rating_key').toString()) { $('#metadata-grandparent_title-' + key) .attr('href', page('info', s.grandparent_rating_key)) .attr('title', s.original_title || s.grandparent_title) .text(s.original_title || s.grandparent_title); $('#metadata-title-' + key) .attr('href', page('info', s.rating_key)) .attr('title', s.title) .text(s.title); } } // Update the transcode state var transcode_decision = ''; if (s.transcode_decision === 'transcode') { var throttled = (s.transcode_throttled === 1) ? ' (Throttled)' : ' (Speed: ' + s.transcode_speed + ')'; transcode_decision = 'Transcode' + throttled; } else if (s.transcode_decision === 'copy') { transcode_decision = 'Direct Stream'; } else { transcode_decision = 'Direct Play'; } $('#transcode_decision-' + key).html(transcode_decision); var transcode_container = ''; if (s.stream_container_decision === 'transcode') { transcode_container = 'Converting (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')'; } else { transcode_container = 'Direct Play (' + s.stream_container.toUpperCase() + ')'; } $('#transcode_container-' + key).html(transcode_container); var video_decision = ''; if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) { var v_bd = (s.video_dynamic_range !== 'SDR') ? ' ' + s.video_dynamic_range : ''; var sv_bd = (s.stream_video_dynamic_range !== 'SDR' || v_bd) ? ' ' + s.stream_video_dynamic_range : ''; var v_res= ''; switch (s.video_resolution.toLowerCase()) { case 'sd': v_res = 'SD'; break; case '4k': v_res = '4k'; break; default: v_res = s.video_full_resolution; } var sv_res = ''; switch (s.stream_video_resolution.toLowerCase()) { case 'sd': sv_res = 'SD'; break; case '4k': sv_res = '4k'; break; default: sv_res = s.stream_video_full_resolution; } if (s.stream_video_decision === 'transcode') { var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : ''; var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : ''; video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + v_bd + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + sv_bd + ')'; } else if (s.stream_video_decision === 'copy') { video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')'; } else { video_decision = 'Direct Play (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')'; } } else if (s.media_type === 'photo') { video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')'; } $('#video_decision-' + key).html(video_decision); var audio_decision = ''; if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.stream_audio_decision) { var audio_language = (s.media_type !== 'track') ? (s.audio_language || 'Unknown') + ' - ' : ''; var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase(); var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase(); if (s.stream_audio_decision === 'transcode') { audio_decision = 'Transcode (' + audio_language + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' <i class="fa fa-long-arrow-right"></i> ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')'; } else if (s.stream_audio_decision === 'copy') { audio_decision = 'Direct Stream (' + audio_language + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')'; } else { audio_decision = 'Direct Play (' + audio_language + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')'; } } $('#audio_decision-' + key).html(audio_decision); var subtitle_decision = 'None'; if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) { var subtitle_codec = (s.stream_subtitle_codec && s.stream_subtitle_transient) ? 'None' : s.subtitle_codec.toUpperCase(); if (s.stream_subtitle_decision === 'transcode') { subtitle_decision = 'Transcode ('+ (s.subtitle_language || 'Unknown')+ ' - ' + subtitle_codec + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')'; } else if (s.stream_subtitle_decision === 'copy') { subtitle_decision = 'Direct Stream ('+ (s.subtitle_language || 'Unknown')+ ' - ' + subtitle_codec + ')'; } else if (s.stream_subtitle_decision === 'burn') { subtitle_decision = 'Burn ('+ (s.subtitle_language || 'Unknown')+ ' - ' + subtitle_codec + ')'; } else { subtitle_decision = 'Direct Play ('+ (s.subtitle_language || 'Unknown')+ ' - ' + ((s.synced_version === '1') ? subtitle_codec : s.stream_subtitle_codec.toUpperCase()) + ')'; } } $('#subtitle_decision-' + key).html(subtitle_decision); // Update the stream quality profile and bandwidth if (s.media_type !== 'photo' && s.quality_profile !== 'Unknown') { var br = parseInt(s.stream_bitrate) || ''; if (br) { if (br > 1000) { br = ' (' + (br / 1000).toFixed(1) + ' Mbps)'; } else { br = ' (' + br + ' kbps)'; } } $('#stream_quality-' + key).html(s.quality_profile + br); } else { $('#stream_quality-' + key).html(s.quality_profile); } $('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')'); $('#synced_quality_profile-' + key).html(s.synced_quality_profile); $('#location-' + key).html(s.location.toUpperCase()); if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') { var bw = parseInt(s.bandwidth) || 0; if (bw > 1000000) { bw = (bw / 1000000).toFixed(1) + ' Gbps'; } else if (bw > 1000) { bw = (bw / 1000).toFixed(1) + ' Mbps'; } else { bw = bw + ' kbps' } $('#stream-bandwidth-' + key).html(bw); } // Update the stream progress times $('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format)); $('#stream-duration-' + key).html(millisecondsToMinutes(parseInt(s.stream_duration), false)); var stream_view_offset = $('#stream-view-offset-' + key); stream_view_offset.data('state', s.state); if (stream_view_offset.data('last_view_offset') !== s.view_offset) { stream_view_offset.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); } // Update the progress bars var duration = parseInt(s.duration); var transcode_progress = duration ? Math.round(s.transcode_max_offset_available * 1000 / duration * 100) : s.transcode_progress; $('#buffer-bar-' + key).css({width: parseInt(transcode_progress) + '%'}).html(transcode_progress + '%') .attr('data-original-title', 'Transcoder Progress ' + transcode_progress + '%'); if (s.live !== 1) { var progress_bar = $('#progress-bar-' + key); progress_bar.data('state', s.state); if (progress_bar.data('last_view_offset') !== s.view_offset) { progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); } } // Add temporary class so we know which instances are still active instance.addClass('updated-temp'); }); // Remove finished instances $('div[id^=activity-instance-]').each(function (i, instance) { if ($(instance).hasClass('updated-temp')) { $(instance).removeClass('updated-temp'); } else { $(instance).find('[data-toggle=tooltip]').tooltip('destroy'); $(instance).find('[data-toggle=popover]').popover('destroy'); $(instance).remove(); } }); } else { $('#currentActivityHeader').hide(); $('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>'); } activity_ready = true; } }); } function getActivityInstance(session_key) { $.ajax({ url: 'get_current_activity_instance', type: 'GET', cache: false, async: true, data: { session_key: session_key }, complete: function(xhr, status) { var instance = $('#activity-instance-' + session_key); if (instance.length) { instance.replaceWith(xhr.responseText); } else { $('#currentActivity').append(xhr.responseText); } $('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar(); $('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 }); $('#activity-instance-' + session_key + ' [data-toggle=popover]').popover({ html: true, sanitize: false, container: 'body', trigger: 'hover', placement: 'right', delay: 50, template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>', content: function () { return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />'; } }); $('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 }); lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller'); var index = create_instances.indexOf(session_key); if (index > -1) { create_instances.splice(index, 1); } } }); } function activityConnected() { getCurrentActivity(); setInterval(function () { if (!(create_instances.length) && activity_ready) { getCurrentActivity(); } }, ${config['home_refresh_interval'] * 1000}); setInterval(function(){ $('.progress_time_offset').each(function () { if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) { var view_offset = parseInt($(this).data('view_offset')); var stream_duration = parseInt($(this).data('stream_duration')); var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false); $(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration)) } }); $('.progress-bar').each(function () { if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) { var view_offset = parseInt($(this).data('view_offset')); var stream_duration = parseInt($(this).data('stream_duration')); var progress_percent = Math.floor(view_offset / stream_duration * 100); progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100; $(this).css({width: progress_percent + '%'}).html(progress_percent + '%') .attr('data-original-title', 'Stream Progress ' + progress_percent + '%') .data('view_offset', Math.min(view_offset + 1000, stream_duration)); } }); }, 1000); } $('#currentActivity').on('click', '.external_ip-modal', function () { $.get('get_ip_address_details', { ip_address: $(this).data('ip'), location: $(this).data('location'), secure: $(this).data('secure'), relayed: $(this).data('relayed') }).then(function (jqXHR) { $("#ip-info-modal").html(jqXHR); }); }); $('#currentActivity').on('click', '.raw-stream-info-modal', function () { $.ajax({ url: 'get_activity', type: 'GET', cache: false, async: true, data: { session_key: $(this).data('key') }, error: function (xhr, status, error) { console.log(status + ': ' + error); }, complete: function (xhr, status) { var session = $.parseJSON(xhr.responseText); $('#raw-stream-info-title').text(session.full_title); $('#raw-stream-info-user').text(session.friendly_name); $('#raw-stream-info').text(JSON.stringify(session, Object.keys(session).sort(), 4)); } }); }); % if _session['user_group'] == 'admin': // Terminate session $('#currentActivity').on('click', '.dashboard-activity-terminate-session', function (e) { e.preventDefault(); var session_id = $(this).data('id'); var key = $(this).data('key'); var instance = $('#activity-instance-' + key); $('#terminate-user').text(instance.find('.dashboard-activity-metadata-user').text()); $('#terminate-title').text(instance.find('.dashboard-activity-metadata-title').text()); $('#terminate-subtitle').text(instance.find('.dashboard-activity-metadata-subtitle').text()); $('#terminate-session-modal').modal(); $('#terminate-session-modal').one('click', '#confirm-terminate-session', function () { var message = $('#terminate-message').val(); showMsg('Terminating session', true); $.ajax({ url: 'terminate_session', data: { session_key: key, session_id: session_id, message: message }, async: true, success: function (data) { if (data.result === 'success') { setTimeout(function() { getCurrentActivity(); showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000); }, 2500); } else { showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true); } } }); }); }); $('#sessions-xml').on('tripleclick', function () { openPlexXML('/status/sessions'); }); % endif </script> % endif % if 'watch_stats' in config['home_sections'] or 'library_stats' in config['home_sections']: <script> function statsCardCallback() { $('.dashboard-stats-instance .dashboard-stats-info-scroller').scrollbar(); loadAllBlurHash(); function changeImages(elem) { var stat_id = $(elem).data('stat_id'); var art = $(elem).data('art'); var thumb = $(elem).data('thumb'); var user_id = $(elem).data('user_id'); var library_type = $(elem).data('library-type'); var user_thumb = $(elem).data('user_thumb'); var rating_key = $(elem).data('rating_key'); var grandparent_rating_key = $(elem).data('grandparent_rating_key'); var guid = $(elem).data('guid'); var live = $(elem).data('live'); var library_art = $(elem).data('library_art'); var library_thumb = $(elem).data('library_thumb'); var [height, fallback_poster, fallback_art] = [450, 'poster', 'art']; if ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) { [height, fallback_poster, fallback_art] = [300, 'cover', 'art']; } else if (live) { [height, fallback_poster, fallback_art] = [450, 'poster-live', 'art-live']; } var href = '#'; if (stat_id === 'most_concurrent') { return } else if (stat_id === 'top_libraries') { $('#stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art || library_art, null, 500, 280, 40, '282828', 3, fallback_art) + ')'); $('#stats-thumb-' + stat_id).removeClass(function (index, className) { return (className.match (/(^|\s)svg-icon library-\S+/g) || []).join(' ')}); if (library_thumb.startsWith('http')) { $('#stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', library_thumb, null, 100, 100, null, null, null, 'cover') + ')'); } else { $('#stats-thumb-' + stat_id).css('background-image', '') .addClass('svg-icon library-' + library_type); } } else if (stat_id === 'top_users') { loadBlurHash($('#stats-background-' + stat_id), page('pms_image_proxy', user_thumb || 'interfaces/default/images/gravatar-default.png', null, 100, 100, 40, '282828', 0, 'user')); $('#stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', user_thumb || 'interfaces/default/images/gravatar-default.png', null, 100, 100, null, null, null, 'user') + ')'); if (user_id) { href = page('user', user_id); } $('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('user')); } else if (stat_id === 'top_platforms') { $('#stats-thumb-' + stat_id).removeClass(function (index, className) { return (className.match (/(^|\s)platform-\S+/g) || []).join(' '); }).addClass('platform-' + $(elem).data('platform')); $('#stats-background-' + stat_id).removeClass(function (index, className) { return (className.match (/(^|\s)platform-\S+/g) || []).join(' '); }).addClass('platform-' + $(elem).data('platform') + '-rgba'); } else { if (rating_key) { if (live) { href = page('info', rating_key, guid, true, live); } else { href = page('info', rating_key); } } var img_rating_key = grandparent_rating_key || rating_key; $('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title')); $('#stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art, img_rating_key, 500, 280, 40, '282828', 3, fallback_art) + ')'); $('#stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', thumb, img_rating_key, 300, height, null, null, null, fallback_poster) + ')'); $('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(' + page('pms_image_proxy', thumb, img_rating_key, 300, height, 60, '282828', 3, fallback_poster) + ')'); $('#library-stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art || library_art, img_rating_key, 500, 280, 40, '282828', 3, library_art || fallback_art) + ')'); if (thumb.startsWith('http')) { $('#library-stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', thumb, img_rating_key, 300, 300, null, null, null, 'cover') + ')') .removeClass('svg-icon library-' + stat_id); } else { $('#library-stats-thumb-' + stat_id).css('background-image', '') .addClass('svg-icon library-' + stat_id); } } } $('.dashboard-stats-info-item').mouseenter(function () { changeImages(this); if ($(this).data('stat_id') === 'last_watched') { var friendly_name = $(this).data('friendly_name'); var last_watch = moment($(this).data('last_watch'), 'X').format(date_format); $('#last-watched-header-info').html(friendly_name); } else if ($(this).data('stat_id') === 'most_concurrent') { var started = moment($(this).data('started'), 'X').format(date_format + ' ' + time_format); $('#most-concurrent-header-info').html(started); } }); $('.dashboard-stats-instance').mouseleave(function () { changeImages($(this).find('.dashboard-stats-info-item').first()); if ($(this).data('stat_id') === 'last_watched') { $('#last-watched-header-info').text($(this).find('.dashboard-stats-info-item').first().data('friendly_name')); } else if ($(this).data('stat_id') === 'most_concurrent') { $('#most-concurrent-header-info').text('streams'); } }); } </script> % endif % if 'watch_stats' in config['home_sections']: <script> function getHomeStats(time_range, stats_type) { showMsg("Loading watch statistics...", true, false, 0); $.ajax({ url: 'home_stats', type: 'GET', cache: false, async: true, data: { time_range: time_range, stats_type: stats_type }, complete: function (xhr, status) { $("#home-stats").html(xhr.responseText); $('#ajaxMsg').fadeOut(); lockScroll('#home-stats .dashboard-stats-info-scroller'); statsCardCallback(); } }); } var stats_type = getLocalStorage('home_stats_type', 'plays'); var time_range = getLocalStorage('home_stats_days', 30); $('#watched-stats-' + stats_type).prop('checked', true); $('#watched-stats-' + stats_type).closest('label').addClass('active'); $('#watched-stats-days').val(time_range); getHomeStats(time_range, stats_type); $('input[name=watched-stats-type]').change(function () { stats_type = $(this).filter(':checked').val(); setLocalStorage('home_stats_type', stats_type); getHomeStats(time_range, stats_type); }); $('#watched-stats-days').change(function () { forceMinMax($(this)); time_range = $(this).val(); setLocalStorage('home_stats_days', time_range); getHomeStats(time_range, stats_type); }); $('#watched-stats-days').tooltip({ container: 'body', placement: 'top', html: true }); </script> % endif % if 'library_stats' in config['home_sections']: <script> function getLibraryStats() { $.ajax({ url: 'library_stats', type: 'GET', cache: false, async: true, data: { }, complete: function (xhr, status) { $("#library-stats").html(xhr.responseText); statsCardCallback(); } }); } getLibraryStats(); </script> % endif % if 'recently_added' in config['home_sections']: <script> function recentlyAdded(recently_added_count, recently_added_type) { showMsg("Loading recently added items...", true, false, 0); $.ajax({ url: 'get_recently_added', type: 'GET', async: true, data: { count: recently_added_count, media_type: recently_added_type }, beforeSend: function () { $(".dashboard-recent-media-row").animate({ scrollLeft: 0 }, 1000); }, complete: function (xhr, status) { $("#recentlyAdded").html(xhr.responseText); $('#ajaxMsg').fadeOut(); highlightScrollerButton("#recently-added"); paginateScroller("#recently-added", ".paginate-added"); } }); } var recently_added_count = getLocalStorage('home_stats_recently_added_count', 50); var recently_added_type = getLocalStorage('home_stats_recently_added_type', 'all');; $('#recently-added-toggle-' + recently_added_type).prop('checked', true); $('#recently-added-toggle-' + recently_added_type).closest('label').addClass('active'); $('#recently-added-count').val(recently_added_count); function recentlyAddedConnected() { recentlyAdded(recently_added_count, recently_added_type); } $('#recently-added-toggles').on('change', function () { $('#recently-added-toggles > label').removeClass('active'); selected_filter = $('input[name=recently-added-toggle]:checked', '#recently-added-toggles'); $(selected_filter).closest('label').addClass('active'); recently_added_type = $(selected_filter).val(); setLocalStorage('home_stats_recently_added_type', recently_added_type); recentlyAdded(recently_added_count, recently_added_type); }); $('#recently-added-count').change(function () { forceMinMax($(this)); recently_added_count = $(this).val(); setLocalStorage('home_stats_recently_added_count', recently_added_count); recentlyAdded(recently_added_count, recently_added_type); }); $('#recently-added-count').tooltip({ container: 'body', placement: 'top', html: true }); $('#recently-added-xml').on('tripleclick', function () { openPlexXML('/library/recentlyAdded', false, {'X-Plex-Container-Start': 0, 'X-Plex-Container-Size': recently_added_count}); }); </script> % endif % if _session['user_group'] == 'admin' and config['update_show_changelog']: <script> $.ajax({ url: 'get_changelog', data: { since_prev_release: true, update_shown: true }, cache: false, async: true, complete: function (xhr, status) { $("#changelog-modal .modal-body").html(xhr.responseText); $('#changelog-modal').modal({ backdrop: 'static', keyboard: false }); } }); </script> % endif </%def>