Add individual library page

This commit is contained in:
Jonathan Wong 2016-01-02 16:02:22 -08:00
parent 979d68957e
commit 5fedac691d
13 changed files with 836 additions and 393 deletions

View file

@ -504,9 +504,9 @@ textarea.form-control:focus {
background-size: contain; background-size: contain;
height: 40px; height: 40px;
width: 40px; width: 40px;
-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); /*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);*/
} }
a .poster-face:hover, a .poster-face:hover,
a .cover-face:hover, a .cover-face:hover,
@ -1664,7 +1664,6 @@ a:hover .item-children-poster {
} }
.user-player-instance-box { .user-player-instance-box {
float: left; float: left;
width: 75px;
border-radius: 3px; border-radius: 3px;
-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
@ -1686,6 +1685,7 @@ a:hover .item-children-poster {
font-weight: normal; font-weight: normal;
width: 140px; width: 140px;
margin-left: 10px; margin-left: 10px;
margin-bottom: 10px;
} }
.user-player-instance-playcount h3 { .user-player-instance-playcount h3 {
font-size: 30px; font-size: 30px;
@ -1705,6 +1705,35 @@ a:hover .item-children-poster {
top: 15px; top: 15px;
left: 0px; left: 0px;
} }
.library-info-poster-face {
float: left;
margin-top: 15px;
margin-right: 15px;
background-size: contain;
height: 80px;
width: 80px;
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);*/
}
.library-user-instance-box {
float: left;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
background-size: contain;
position: relative;
height: 80px;
width: 80px;
}
.library-user-instance-box:hover {
-webkit-box-shadow: inset 0 0 0 2px #e9a049;
-moz-box-shadow: inset 0 0 0 2px #e9a049;
box-shadow: inset 0 0 0 2px #e9a049;
}
.home-platforms { .home-platforms {
} }
.home-platforms ul { .home-platforms ul {

View file

@ -0,0 +1,171 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: edit_library.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
section_id Returns the library id of the library.
section_name Returns the name of the library.
section_type Returns the type of the library.
library_thumb Returns the thumbnail for the library.
count Returns the item count for the library.
parent_count Returns the parent item count for the library.
child_count Returns the child item count for the library.
do_notify Returns bool value for whether to send notifications for the library.
keep_history Returns bool value for whether to keep history for the library.
DOCUMENTATION :: END
</%doc>
<%!
from plexpy import helpers
%>
% if data != None:
<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">Edit library <strong>${data['section_name']}</strong></h4>
</div>
<div class="modal-body" id="modal-text">
<fieldset>
<div class="form-group">
<label for="profile_url">Library Picture URL</label>
<div class="row">
<div class="col-md-8">
% if data['custom_thumb']:
<input type="text" class="form-control" id="custom_thumb_url" name="custom_thumb_url" value="${data['custom_thumb']}">
% else:
<input type="text" class="form-control" id="custom_thumb_url" name="custom_thumb_url" value="${data['library_thumb']}">
% endif
</div>
</div>
<p class="help-block">Change the library's picture in PlexPy. To reset to default, leave this field empty and save.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${helpers.checked(data['do_notify'])}> Enable notifications
</label>
<p class="help-block">Uncheck this if you do not want to receive notifications for this library's activity.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history
</label>
<p class="help-block">Uncheck this if you do not want this keep any history on this library's activity.</p>
</div>
% if data['section_id']:
<div class="form-group">
<button class="btn btn-danger" id="delete-all-history">Purge</button>
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this library. This is permanent!</p>
</div>
% endif
</fieldset>
</div>
<div class="modal-footer">
<div>
<span id="edit-library-status-message"></span>
<input type="button" id="save_library" class="btn btn-bright" value="Save">
</div>
</div>
</div>
</div>
<div id="confirm-modal-purge" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="confirm-modal-purge">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Purge</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to purge all history for this library?</p>
<p>This is permanent and cannot be undone!</p>
</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-purge">Purge</button>
</div>
</div>
</div>
</div>
<script>
// Set new friendly name
$("#save_library").click(function () {
var custom_thumb = $("#custom_thumb_url").val();
var do_notify = 0;
var keep_history = 0;
if ($("#do_notify").is(":checked")) {
do_notify = 1;
}
if ($("#keep_history").is(":checked")) {
keep_history = 1;
}
$.ajax({
url: 'edit_library',
data: {
section_id: '${data["section_id"]}',
do_notify: do_notify,
keep_history: keep_history,
custom_thumb: custom_thumb
},
cache: false,
async: true,
success: function (data) {
location.reload();
}
});
});
$("#delete-all-history").on('click', function() {
$('#confirm-modal-purge').modal();
$('#confirm-modal-purge').one('click', '#confirm-purge', function () {
$.ajax({
url: 'delete_all_library_history',
data: { section_id: '${data["section_id"]}' },
cache: false,
async: true,
success: function(data) {
location.reload();
}
});
});
});
$(document).ready(function() {
// Move #confirm-modal to parent container
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) {
$('#confirm-modal-purge').appendTo($('#edit-library-modal').parent());
}
$('#edit-library-modal > #confirm-modal-purge').remove();
$('#edit-library-modal').css('z-index', '1050');
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1049');
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
$('#confirm-modal-purge').on('show.bs.modal', function () {
// Fix position to match parent modal
var currentPadding = parseInt($('body').css('padding-right'));
$(this).children('.modal-dialog').css('left', -currentPadding/2);
$('#edit-library-modal').css('overflow-y', 'hidden');
});
$('#confirm-modal-purge').on('shown.bs.modal', function () {
$(this).css('z-index', '1060');
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1059');
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
});
$('#confirm-modal-purge').on('hidden.bs.modal', function () {
$('body').addClass('modal-open');
$('#edit-library-modal').css('overflow-y', 'auto');
});
});
</script>
% endif

View file

@ -53,32 +53,30 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="summary-navbar-list"> <div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb"> <ul class="list-unstyled breadcrumb">
% if data['media_type'] == 'library': % if data['media_type'] == 'movie':
<li class="active">${data['title']}</li> <li><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
% elif data['media_type'] == 'movie':
<li><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['media_type'] == 'show': % elif data['media_type'] == 'show':
<li><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li> <li><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['media_type'] == 'season': % elif data['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Season ${data['media_index']}</li> <li class="active">Season ${data['media_index']}</li>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
<li class="active">Episode ${data['media_index']} - ${data['title']}</li> <li class="active">Episode ${data['media_index']} - ${data['title']}</li>
% elif data['media_type'] == 'artist': % elif data['media_type'] == 'artist':
<li><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li> <li><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['media_type'] == 'album': % elif data['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="info?library_id=${data['library_id']}">${data['library_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="library?section_id=${data['library_id']}">${data['library_title']}</a></li>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Track ${data['media_index']} - ${data['title']}</li> <li class="active">Track ${data['media_index']} - ${data['title']}</li>
@ -87,13 +85,12 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% if data['media_type'] != 'library':
<div class="summary-content-title-wrapper"> <div class="summary-content-title-wrapper">
<div class="col-md-9"> <div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm"> <div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track': % if data['media_type'] == 'track':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web"> <a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web">
% elif data['media_type'] != 'library': % else:
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web"> <a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web">
% endif % endif
% if data['media_type'] == 'episode': % if data['media_type'] == 'episode':
@ -108,7 +105,7 @@ DOCUMENTATION :: END
<span></span> <span></span>
</div> </div>
</div> </div>
% elif data['media_type'] != 'library': % else:
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"> <div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
@ -138,9 +135,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% endif
<div class="summary-content-wrapper"> <div class="summary-content-wrapper">
% if data['media_type'] != 'library':
<div class="col-md-9"> <div class="col-md-9">
% if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'season': % if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'season':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div>
@ -292,8 +287,8 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
% endif % endif
% endif
<div class='col-md-12'> <div class='col-md-12'>
<!-- Need to find a place to put this -->
% if data['media_type'] == 'library' and config['update_library_ids'] == 1: % if data['media_type'] == 'library' and config['update_library_ids'] == 1:
<div id="update_library_ids_mssage" style="text-align: center; margin-top: 20px;"> <div id="update_library_ids_mssage" style="text-align: center; margin-top: 20px;">
<i class="fa fa-refresh fa-spin"></i> Updating library ids in the database. This could take a few minutes depending on the size of your database. <i class="fa fa-refresh fa-spin"></i> Updating library ids in the database. This could take a few minutes depending on the size of your database.
@ -374,6 +369,7 @@ DOCUMENTATION :: END
% if data: % if data:
<script src="interfaces/default/js/tables/history_table.js"></script> <script src="interfaces/default/js/tables/history_table.js"></script>
<!-- Need to find a place to put this -->
% if data['media_type'] == 'library': % if data['media_type'] == 'library':
<script> <script>
function get_history() { function get_history() {
@ -499,13 +495,7 @@ DOCUMENTATION :: END
}); });
</script> </script>
% endif % endif
% if data['media_type'] != 'library': % if data['rating']:
<script>
$('#row-edit-mode').after('<a href="update_metadata?rating_key=${data['rating_key']}" class="btn btn-danger btn-edit" id="fix-metadata"> \
<i class="fa fa-wrench"></i> Fix Metadata</a>');
</script>
% endif
% if data['media_type'] != 'library' and data['rating']:
<script> <script>
// Convert rating to 5 star rating type // Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2); var starRating = Math.round(${data['rating']} / 2);
@ -513,6 +503,8 @@ DOCUMENTATION :: END
</script> </script>
% endif % endif
<script> <script>
$('#row-edit-mode').after('<a href="update_metadata?rating_key=${data['rating_key']}" class="btn btn-danger btn-edit" id="fix-metadata"> \
<i class="fa fa-wrench"></i> Fix Metadata</a>');
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY')); $("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true)); $("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 }); $('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });

View file

@ -37,10 +37,14 @@ libraries_list_table_options = {
"data": "library_thumb", "data": "library_thumb",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === '') { if (cellData === '') {
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(interfaces/default/images/gravatar-default-80x80.png);"></div></a>'); $(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(pms_image_proxy?img=' + rowData['library_thumb'] + '&width=80&height=80&fallback=poster);"></div></a>');
} else {
if (rowData['custom_thumb']) {
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(' + rowData['custom_thumb'] + ');"></div></a>');
} else { } else {
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(pms_image_proxy?img=' + rowData['library_thumb'] + '&width=80&height=80&fallback=poster);"></div></a>'); $(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(pms_image_proxy?img=' + rowData['library_thumb'] + '&width=80&height=80&fallback=poster);"></div></a>');
} }
}
}, },
"orderable": false, "orderable": false,
"searchable": false, "searchable": false,
@ -213,6 +217,11 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
if ($('#keep_history-' + rowData['section_id']).is(':checked')) { if ($('#keep_history-' + rowData['section_id']).is(':checked')) {
keep_history = 1; keep_history = 1;
} }
if (rowData['custom_thumb']) {
custom_thumb = rowData['custom_thumb']
} else {
custom_thumb = rowData['library_thumb']
}
$.ajax({ $.ajax({
url: 'edit_library', url: 'edit_library',
@ -220,7 +229,7 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
section_id: rowData['section_id'], section_id: rowData['section_id'],
do_notify: do_notify, do_notify: do_notify,
keep_history: keep_history, keep_history: keep_history,
custom_thumb: rowData['library_thumb'] custom_thumb: custom_thumb
}, },
cache: false, cache: false,
async: true, async: true,

View file

@ -103,7 +103,7 @@
for (var i = 0; i < libraries_to_purge.length; i++) { for (var i = 0; i < libraries_to_purge.length; i++) {
$.ajax({ $.ajax({
url: 'delete_all_library_history', url: 'delete_all_library_history',
data: { library_id: libraries_to_purge[i] }, data: { section_id: libraries_to_purge[i] },
cache: false, cache: false,
async: true, async: true,
success: function (data) { success: function (data) {

View file

@ -0,0 +1,365 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: library.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
section_id Returns the library id of the library.
section_name Returns the name of the library.
section_type Returns the type of the library.
library_thumb Returns the thumbnail for the library.
count Returns the item count for the library.
parent_count Returns the parent item count for the library.
child_count Returns the child item count for the library.
do_notify Returns bool value for whether to send notifications for the library.
keep_history Returns bool value for whether to keep history for the library.
DOCUMENTATION :: END
</%doc>
<%inherit file="base.html"/>
<%def name="headIncludes()">
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
<link rel="stylesheet" href="interfaces/default/css/dataTables.colVis.css">
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
</%def>
% if data != None:
<%def name="body()">
<div class="container-fluid">
<div class="row">
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['library_art']}&width=1920&height=1080)"></div>
<div class="summary-container">
<div class="summary-navbar">
<div class="col-md-12">
<div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb">
<li class="active">${data['section_name']}</li>
</ul>
</div>
</div>
</div>
<div class="summary-content-wrapper">
<div class="col-md-12">
<div class="table-card-back">
<div class="user-info-wrapper">
% if data['custom_thumb']:
<div class="library-info-poster-face" id="user-gravatar" style="background-image: url(${data['custom_thumb']});">
% else:
<div class="library-info-poster-face" id="user-gravatar" style="background-image: url(pms_image_proxy?img=${data['library_thumb']}&width=80&height=80&fallback=poster);">
% endif
</div>
<div class="user-info-username">
<span class="set-username">${data['section_name']}</span>
<span id="edit-library-tooltip" data-target="tooltip" title="Edit library details">
<a href="#" data-toggle="modal" data-target="#edit-library-modal" id="toggle-edit-library-modal"><i class="fa fa-pencil"></i></a>
</span>
</div>
<div class="user-info-nav">
<ul class="user-info-nav">
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#libraryHistory" data-toggle="tab">History</a></li>
</ul>
</div>
</div>
</div>
</div>
<div id="edit-library-modal" class="modal fade" tabindex="-1" role="dialog"
aria-labelledby="edit-library-modal">
</div>
<div class="tab-content">
<div class="tab-pane active" id="profile">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span><i class="fa fa-line-chart"></i> Global Stats</span>
</div>
</div>
<div class="table-card-back">
<div id="library-time-stats" class="user-overview-stats-wrapper">
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span><i class="fa fa-group"></i> User Stats</span>
</div>
</div>
<div class="table-card-back">
<div id="library-user-stats" class="user-player">
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span><i class="fa fa-history"></i> Recently Watched</span>
</div>
</div>
<div class="table-card-back">
<div id="library-recently-watched">
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div>
<br>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="libraryHistory">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class='table-card-header'>
<div class="header-bar">
<span>
<i class="fa fa-history"></i> Watch History for <strong>
<span class="set-username">${data['section_name']}</span>
</strong>
</span>
</div>
<div class="button-bar">
<div class="colvis-button-bar hidden-xs" id="button-bar-history"></div>
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
<i class="fa fa-trash-o"></i> Delete mode
</button>
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect rows to delete. Data is deleted upon exiting delete mode.</div>
</div>
</div>
<div class="table-card-back">
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="player">Player</th>
<th align='left' id="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Delete</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
<p>This is permanent and cannot be undone!</p>
</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-delete">Delete</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer></footer>
</%def>
<%def name="javascriptIncludes()">
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
<script src="interfaces/default/js/dataTables.colVis.js"></script>
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
<script src="interfaces/default/js/moment-with-locale.js"></script>
<script src="interfaces/default/js/tables/history_table.js"></script>
<script>
$(document).ready(function () {
% if data['section_id']:
var section_id = ${data['section_id']};
% else:
var section_id = null;
% endif
var section_name = '${data['section_name'].replace("'", "\\'")}';
$("#edit-library-tooltip").tooltip();
// Populate watch time stats
$.ajax({
url: 'get_library_watch_time_stats',
async: true,
data: { library_id: section_id },
complete: function(xhr, status) {
$("#library-time-stats").html(xhr.responseText);
}
});
// Populate user stats
$.ajax({
url: 'get_library_user_stats',
async: true,
data: { library_id: section_id },
complete: function(xhr, status) {
$("#library-user-stats").html(xhr.responseText);
}
});
function loadHistoryTable() {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
'json_data': JSON.stringify( d ),
'section_id': section_id
};
}
}
history_table = $('#history_table').DataTable(history_table_options);
//history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table);
}
$( "#history-tab-btn" ).one( "click", function() {
loadHistoryTable();
});
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-delete', function () {
for (var i = 0; i < history_to_delete.length; i++) {
$.ajax({
url: 'delete_history_rows',
data: { row_id: history_to_delete[i] },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
}
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
function recentlyWatched() {
var widthVal = $('body').find("#library-recently-watched").width();
var tmp = (widthVal-32) / 180;
if (tmp > 0) {
containerSize = parseInt(tmp);
} else {
containerSize = 1;
}
// Populate recently watched
$.ajax({
url: 'get_library_recently_watched',
async: true,
data: {
library_id: section_id,
limit: containerSize
},
complete: function(xhr, status) {
$("#library-recently-watched").html(xhr.responseText);
}
});
}
recentlyWatched();
$(window).resize(function() {
recentlyWatched();
});
});
</script>
<script>
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
</script>
</%def>
% else:
<div class="clear"></div>
<div class="container-fluid">
<div class="row-fluid">
<div class="span10 offset1">
<h3>Error retrieving library information. Please see the logs for more details.</h3>
</div>
</div>
</div>
% endif

View file

@ -30,13 +30,9 @@ DOCUMENTATION :: END
<li> <li>
<div class="home-platforms-instance-info"> <div class="home-platforms-instance-info">
<div class="home-platforms-instance-name"> <div class="home-platforms-instance-name">
% if library['section_type'] != 'photo':
<h4> <h4>
<a href="info?library_id=${library['section_id']}" title="${library['section_name']}">${library['section_name']}</a> <a href="library?section_id=${library['section_id']}" title="${library['section_name']}">${library['section_name']}</a>
</h4> </h4>
% else:
<h4>${library['section_name']}</h4>
% endif
</div> </div>
% if library['section_type'] == 'movie': % if library['section_type'] == 'movie':
<div class="home-platforms-instance-playcount"> <div class="home-platforms-instance-playcount">

View file

@ -0,0 +1,43 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: library_user_stats.html
Version: 0.1
Variable names: data [array]
data[array_index] :: Usable parameters
== Global keys ==
user Returns the name of the user.
user_id Returns the user id of the user.
thumb Returns the avatar of the user.
total_plays Returns the play count for the user.
DOCUMENTATION :: END
</%doc>
% if data != None:
% for a in data:
<ul class="list-unstyled">
<div class="user-player-instance">
<li>
<a href="user?user_id=${a['user_id']}" title="${a['user']}">
<div class="library-user-instance-box" style="background-image: url(${a['thumb']});"></div>
</a>
<div class=" user-player-instance-name">
<a href="user?user_id=${a['user_id']}" title="${a['user']}">${a['user']}</a>
</div>
<div class="user-player-instance-playcount">
<h3>${a['total_plays']}</h3>
<p> plays</p>
</div>
</li>
</div>
</ul>
% endfor
% else:
<div class="text-muted">Unable to retrieve data from database. Please check your <a href="settings">settings</a>.
</div><br>
% endif

View file

@ -440,7 +440,7 @@ def dbcheck():
c_db.execute( c_db.execute(
'CREATE TABLE IF NOT EXISTS library_sections (id INTEGER PRIMARY KEY AUTOINCREMENT, ' 'CREATE TABLE IF NOT EXISTS library_sections (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'server_id TEXT, section_id INTEGER UNIQUE, section_name TEXT, section_type TEXT, ' 'server_id TEXT, section_id INTEGER UNIQUE, section_name TEXT, section_type TEXT, '
'thumb TEXT, custom_thumb_url TEXT, count INTEGER, parent_count INTEGER, child_count INTEGER, ' 'thumb TEXT, custom_thumb_url TEXT, art TEXT, count INTEGER, parent_count INTEGER, child_count INTEGER, '
'do_notify INTEGER DEFAULT 1, keep_history INTEGER DEFAULT 1)' 'do_notify INTEGER DEFAULT 1, keep_history INTEGER DEFAULT 1)'
) )

View file

@ -725,7 +725,7 @@ class DataFactory(object):
return stream_output return stream_output
def get_recently_watched(self, user=None, user_id=None, limit='10'): def get_recently_watched(self, user=None, user_id=None, library_id=None, limit='10'):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
recently_watched = [] recently_watched = []
@ -755,6 +755,17 @@ class DataFactory(object):
' ELSE session_history.rating_key END) ' \ ' ELSE session_history.rating_key END) ' \
'ORDER BY started DESC LIMIT ?' 'ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user, limit]) result = monitor_db.select(query, args=[user, limit])
elif library_id:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \
'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
'year, started, user ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE library_id = ? ' \
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
' ELSE session_history.rating_key END) ' \
'ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[library_id, limit])
else: else:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \ query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \
'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \ 'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \
@ -1177,7 +1188,9 @@ class DataFactory(object):
try: try:
query = 'SELECT SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - ' \ query = 'SELECT SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - ' \
'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS total_duration ' \ 'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS total_duration ' \
'FROM session_history %s ' % where 'FROM session_history ' \
'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \
'%s ' % where
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query for get_total_duration.") logger.warn("Unable to execute database query for get_total_duration.")

View file

@ -30,8 +30,10 @@ class Libraries(object):
'library_sections.count as count', 'library_sections.count as count',
'library_sections.parent_count', 'library_sections.parent_count',
'library_sections.child_count', 'library_sections.child_count',
'(CASE WHEN library_sections.custom_thumb_url IS NULL THEN library_sections.thumb ELSE ' \ 'library_sections.thumb AS library_thumb',
'custom_thumb_url END) AS library_thumb', '(CASE WHEN library_sections.custom_thumb_url == library_sections.thumb \
THEN NULL ELSE custom_thumb_url END) AS custom_thumb',
'library_sections.art',
'COUNT(session_history.id) as plays', 'COUNT(session_history.id) as plays',
'MAX(session_history.started) as last_accessed', 'MAX(session_history.started) as last_accessed',
'session_history_metadata.full_title as last_watched', 'session_history_metadata.full_title as last_watched',
@ -91,6 +93,8 @@ class Libraries(object):
'count': item['count'], 'count': item['count'],
'parent_count': item['parent_count'], 'parent_count': item['parent_count'],
'library_thumb': item['library_thumb'], 'library_thumb': item['library_thumb'],
'custom_thumb': item['custom_thumb'],
'library_art': item['art'],
'child_count': item['child_count'], 'child_count': item['child_count'],
'do_notify': helpers.checked(item['do_notify']), 'do_notify': helpers.checked(item['do_notify']),
'keep_history': helpers.checked(item['keep_history']) 'keep_history': helpers.checked(item['keep_history'])
@ -106,93 +110,6 @@ class Libraries(object):
return dict return dict
def get_user_unique_ips(self, kwargs=None, custom_where=None):
data_tables = datatables.DataTables()
# Change custom_where column name due to ambiguous column name after JOIN
custom_where[0][0] = 'custom_user_id' if custom_where[0][0] == 'user_id' else custom_where[0][0]
columns = ['session_history.id',
'session_history.started as last_seen',
'session_history.ip_address as ip_address',
'COUNT(session_history.id) as play_count',
'session_history.platform as platform',
'session_history.player as player',
'session_history_metadata.full_title as last_watched',
'session_history_metadata.thumb',
'session_history_metadata.parent_thumb',
'session_history_metadata.grandparent_thumb',
'session_history_metadata.media_type',
'session_history.rating_key as rating_key',
'session_history_media_info.video_decision',
'session_history.user as user',
'session_history.user_id as custom_user_id',
'(case when users.friendly_name is null then users.username else \
users.friendly_name end) as friendly_name'
]
try:
query = data_tables.ssp_query(table_name='session_history',
columns=columns,
custom_where=custom_where,
group_by=['ip_address'],
join_types=['JOIN',
'JOIN',
'JOIN'],
join_tables=['users',
'session_history_metadata',
'session_history_media_info'],
join_evals=[['session_history.user_id', 'users.user_id'],
['session_history.id', 'session_history_metadata.id'],
['session_history.id', 'session_history_media_info.id']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
results = query['result']
rows = []
for item in results:
if item["media_type"] == 'episode' and item["parent_thumb"]:
thumb = item["parent_thumb"]
elif item["media_type"] == 'episode':
thumb = item["grandparent_thumb"]
else:
thumb = item["thumb"]
# Rename Mystery platform names
platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
row = {"id": item['id'],
"last_seen": item['last_seen'],
"ip_address": item['ip_address'],
"play_count": item['play_count'],
"platform": platform,
"player": item['player'],
"last_watched": item['last_watched'],
"thumb": thumb,
"media_type": item['media_type'],
"rating_key": item['rating_key'],
"video_decision": item['video_decision'],
"friendly_name": item['friendly_name']
}
rows.append(row)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
# TODO: The getter and setter for this needs to become a config getter/setter for more than just friendlyname
def set_library_config(self, section_id=None, do_notify=1, keep_history=1, custom_thumb=''): def set_library_config(self, section_id=None, do_notify=1, keep_history=1, custom_thumb=''):
if section_id: if section_id:
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
@ -206,255 +123,110 @@ class Libraries(object):
except: except:
logger.warn("Unable to execute database query for set_user_friendly_name.") logger.warn("Unable to execute database query for set_user_friendly_name.")
def set_user_profile_url(self, user=None, user_id=None, profile_url=None): def get_library_details(self, section_id=None):
if user_id: from plexpy import pmsconnect
if profile_url.strip() == '':
profile_url = None
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
control_value_dict = {"user_id": user_id} if section_id:
new_value_dict = {"custom_avatar_url": profile_url} query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
try: 'thumb AS library_thumb, (CASE WHEN library_sections.custom_thumb_url == library_sections.thumb ' \
monitor_db.upsert('users', new_value_dict, control_value_dict) ' THEN NULL ELSE custom_thumb_url END) AS custom_thumb, art, do_notify, keep_history ' \
except Exception, e: 'FROM library_sections ' \
logger.debug(u"Uncaught exception %s" % e) 'WHERE section_id = ? '
if user: result = monitor_db.select(query, args=[section_id])
if profile_url.strip() == '':
profile_url = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"username": user}
new_value_dict = {"custom_avatar_url": profile_url}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
def get_user_friendly_name(self, user=None, user_id=None):
if user_id:
monitor_db = database.MonitorDatabase()
query = 'select username, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) as friendly_name,' \
'do_notify, keep_history, custom_avatar_url as thumb ' \
'FROM users WHERE user_id = ?'
result = monitor_db.select(query, args=[user_id])
if result:
user_detail = {'user_id': user_id,
'user': result[0]['username'],
'friendly_name': result[0]['friendly_name'],
'thumb': result[0]['thumb'],
'do_notify': helpers.checked(result[0]['do_notify']),
'keep_history': helpers.checked(result[0]['keep_history'])
}
return user_detail
else:
user_detail = {'user_id': user_id,
'user': '',
'friendly_name': '',
'do_notify': '',
'thumb': '',
'keep_history': ''}
return user_detail
elif user:
monitor_db = database.MonitorDatabase()
query = 'select user_id, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) as friendly_name,' \
'do_notify, keep_history, custom_avatar_url as thumb ' \
'FROM users WHERE username = ?'
result = monitor_db.select(query, args=[user])
if result:
user_detail = {'user_id': result[0]['user_id'],
'user': user,
'friendly_name': result[0]['friendly_name'],
'thumb': result[0]['thumb'],
'do_notify': helpers.checked(result[0]['do_notify']),
'keep_history': helpers.checked(result[0]['keep_history'])}
return user_detail
else:
user_detail = {'user_id': None,
'user': user,
'friendly_name': '',
'do_notify': '',
'thumb': '',
'keep_history': ''}
return user_detail
return None
def get_user_id(self, user=None):
if user:
try:
monitor_db = database.MonitorDatabase()
query = 'select user_id FROM users WHERE username = ?'
result = monitor_db.select_single(query, args=[user])
if result:
return result
else:
return None
except:
return None
return None
def get_user_details(self, user=None, user_id=None):
from plexpy import plextv
monitor_db = database.MonitorDatabase()
if user:
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else: else:
result = None result = None
if result: if result:
user_details = {} library_details = {}
for item in result: for item in result:
if not item['friendly_name']: library_details = {'section_id': item['section_id'],
friendly_name = item['username'] 'section_name': item['section_name'],
else: 'section_type': item['section_type'],
friendly_name = item['friendly_name'] 'library_thumb': item['library_thumb'],
if not item['thumb'] or item['thumb'] == '': 'custom_thumb': item['custom_thumb'],
user_thumb = common.DEFAULT_USER_THUMB 'library_art': item['art'],
else: 'count': item['count'],
user_thumb = item['thumb'] 'parent_count': item['parent_count'],
'child_count': item['child_count'],
user_details = {"user_id": item['user_id'], 'do_notify': item['do_notify'],
"username": item['username'], 'keep_history': item['keep_history']
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
} }
return user_details return library_details
else: else:
logger.warn(u"PlexPy :: Unable to retrieve user from local database. Requesting user list refresh.") logger.warn(u"PlexPy :: Unable to retrieve library from local database. Requesting library list refresh.")
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet # Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
if user: if section_id:
# Refresh users # Refresh libraries
plextv.refresh_users() pmsconnect.refresh_libraries()
query = 'SELECT user_id, username, friendly_name, email, ' \ query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \ 'thumb AS library_thumb, (CASE WHEN library_sections.custom_thumb_url == library_sections.thumb ' \
'FROM users ' \ ' THEN NULL ELSE custom_thumb_url END) AS custom_thumb, art, do_notify, keep_history ' \
'WHERE username = ? ' \ 'FROM library_sections ' \
'UNION ALL ' \ 'WHERE section_id = ? '
'SELECT null, user, null, null, null, null, null, null, null ' \ result = monitor_db.select(query, args=[section_id])
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else: else:
result = None result = None
if result: if result:
user_details = {} library_details = {}
for item in result: for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'], library_details = {'section_id': item['section_id'],
"username": item['username'], 'section_name': item['section_name'],
"friendly_name": friendly_name, 'section_type': item['section_type'],
"email": item['email'], 'library_thumb': item['library_thumb'],
"thumb": user_thumb, 'custom_thumb': item['custom_thumb'],
"is_home_user": item['is_home_user'], 'library_art': item['art'],
"is_allow_sync": item['is_allow_sync'], 'count': item['count'],
"is_restricted": item['is_restricted'], 'parent_count': item['parent_count'],
"do_notify": item['do_notify'] 'child_count': item['child_count'],
'do_notify': item['do_notify'],
'keep_history': item['keep_history']
} }
return user_details return user_details
else: else:
# If there is no user data we must return something # If there is no library data we must return something
# Use "Local" user to retain compatibility with PlexWatch database value # Use "Local" user to retain compatibility with PlexWatch database value
return {"user_id": None, return {'section_id': None,
"username": 'Local', 'section_name': '',
"friendly_name": 'Local', 'section_type': '',
"email": '', 'library_thumb': '',
"thumb": '', 'custom_thumb': '',
"is_home_user": 0, 'library_art': '',
"is_allow_sync": 0, 'count': 0,
"is_restricted": 0, 'parent_count': 0,
"do_notify": 0 'child_count': 0,
'do_notify': 0,
'keep_history': 0
} }
def get_user_watch_time_stats(self, user=None, user_id=None): def get_library_watch_time_stats(self, library_id=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
time_queries = [1, 7, 30, 0] time_queries = [1, 7, 30, 0]
user_watch_time_stats = [] library_watch_time_stats = []
for days in time_queries: for days in time_queries:
if days > 0: if days > 0:
if user_id: if library_id:
query = 'SELECT (SUM(stopped - started) - ' \ query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \ 'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \ 'COUNT(session_history.id) AS total_plays ' \
'FROM session_history ' \ 'FROM session_history ' \
'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \ 'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user_id = ?' % days 'AND library_id = ?' % days
result = monitor_db.select(query, args=[user_id]) result = monitor_db.select(query, args=[library_id])
elif user:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user = ?' % days
result = monitor_db.select(query, args=[user])
else: else:
query = 'SELECT (SUM(stopped - started) - ' \ query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \ 'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \ 'COUNT(session_history.id) AS total_plays ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE user = ?' 'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \
result = monitor_db.select(query, args=[user]) 'WHERE library_id = ?'
result = monitor_db.select(query, args=[library_id])
for item in result: for item in result:
if item['total_time']: if item['total_time']:
@ -469,73 +241,64 @@ class Libraries(object):
'total_plays': total_plays 'total_plays': total_plays
} }
user_watch_time_stats.append(row) library_watch_time_stats.append(row)
return user_watch_time_stats return library_watch_time_stats
def get_user_player_stats(self, user=None, user_id=None): def get_library_user_stats(self, library_id=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
player_stats = [] user_stats = []
result_id = 0
try: try:
if user_id: if library_id:
query = 'SELECT player, COUNT(player) as player_count, platform ' \ query = 'SELECT (CASE WHEN users.friendly_name IS NULL THEN users.username ' \
'ELSE users.friendly_name END) AS user, users.user_id, users.thumb, COUNT(user) AS user_count ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE user_id = ? ' \ 'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \
'GROUP BY player ' \ 'JOIN users ON users.user_id = session_history.user_id ' \
'ORDER BY player_count DESC' 'WHERE library_id = ? ' \
result = monitor_db.select(query, args=[user_id]) 'GROUP BY user ' \
else: 'ORDER BY user_count DESC'
query = 'SELECT player, COUNT(player) as player_count, platform ' \ result = monitor_db.select(query, args=[library_id])
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user])
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_library_user_stats.")
return None return None
for item in result: for item in result:
# Rename Mystery platform names row = {'user': item['user'],
platform_type = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']) 'user_id': item['user_id'],
'thumb': item['thumb'],
row = {'player_name': item['player'], 'total_plays': item['user_count']
'platform_type': platform_type,
'total_plays': item['player_count'],
'result_id': result_id
} }
player_stats.append(row) user_stats.append(row)
result_id += 1
return player_stats return user_stats
def delete_all_library_history(self, library_id=None): def delete_all_library_history(self, section_id=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if library_id.isdigit(): if section_id.isdigit():
logger.info(u"PlexPy Libraries :: Deleting all history for library id %s from database." % library_id) logger.info(u"PlexPy Libraries :: Deleting all history for library id %s from database." % section_id)
session_history_media_info_del = \ session_history_media_info_del = \
monitor_db.action('DELETE FROM ' monitor_db.action('DELETE FROM '
'session_history_media_info ' 'session_history_media_info '
'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id ' 'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id '
'FROM session_history_media_info ' 'FROM session_history_media_info '
'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id ' 'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id '
'WHERE session_history_metadata.library_id = ?)', [library_id]) 'WHERE session_history_metadata.library_id = ?)', [section_id])
session_history_del = \ session_history_del = \
monitor_db.action('DELETE FROM ' monitor_db.action('DELETE FROM '
'session_history ' 'session_history '
'WHERE session_history.id IN (SELECT session_history.id ' 'WHERE session_history.id IN (SELECT session_history.id '
'FROM session_history ' 'FROM session_history '
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' 'JOIN session_history_metadata ON session_history.id = session_history_metadata.id '
'WHERE session_history_metadata.library_id = ?)', [library_id]) 'WHERE session_history_metadata.library_id = ?)', [section_id])
session_history_metadata_del = \ session_history_metadata_del = \
monitor_db.action('DELETE FROM ' monitor_db.action('DELETE FROM '
'session_history_metadata ' 'session_history_metadata '
'WHERE session_history_metadata.library_id = ?', [library_id]) 'WHERE session_history_metadata.library_id = ?', [section_id])
return 'Deleted all items for library_id %s.' % library_id return 'Deleted all items for library_id %s.' % section_id
else: else:
return 'Unable to delete items. Input library_id not valid.' return 'Unable to delete items. Input library_id not valid.'

View file

@ -61,6 +61,7 @@ def refresh_libraries():
'section_name': section['title'], 'section_name': section['title'],
'section_type': section['type'], 'section_type': section['type'],
'thumb': section['thumb'], 'thumb': section['thumb'],
'art': section['art'],
'count': section['count'], 'count': section['count'],
'parent_count': section.get('parent_count', None), 'parent_count': section.get('parent_count', None),
'child_count': section.get('child_count', None) 'child_count': section.get('child_count', None)
@ -1460,7 +1461,8 @@ class PmsConnect(object):
libraries_output = {'key': helpers.get_xml_attr(result, 'key'), libraries_output = {'key': helpers.get_xml_attr(result, 'key'),
'type': helpers.get_xml_attr(result, 'type'), 'type': helpers.get_xml_attr(result, 'type'),
'title': helpers.get_xml_attr(result, 'title'), 'title': helpers.get_xml_attr(result, 'title'),
'thumb': helpers.get_xml_attr(result, 'thumb') 'thumb': helpers.get_xml_attr(result, 'thumb'),
'art': helpers.get_xml_attr(result, 'art')
} }
libraries_list.append(libraries_output) libraries_list.append(libraries_output)
@ -1564,6 +1566,7 @@ class PmsConnect(object):
'title': library['title'], 'title': library['title'],
'type': library_type, 'type': library_type,
'thumb': library['thumb'], 'thumb': library['thumb'],
'art': library['art'],
'count': library_list['library_count'], 'count': library_list['library_count'],
'count_type': library_list['count_type'] 'count_type': library_list['count_type']
} }

View file

@ -266,6 +266,32 @@ class WebInterface(object):
status_message = "Failed to update user." status_message = "Failed to update user."
return status_message return status_message
@cherrypy.expose
def library(self, section_id=None):
library_data = libraries.Libraries()
if section_id:
try:
library_details = library_data.get_library_details(section_id=section_id)
except:
logger.warn("Unable to retrieve user details for library_id %s " % section_id)
else:
logger.debug(u"Library page requested but no parameters received.")
raise cherrypy.HTTPRedirect("home")
return serve_template(templatename="library.html", title="Library", data=library_details)
@cherrypy.expose
def edit_library_dialog(self, section_id=None, **kwargs):
library_data = libraries.Libraries()
if section_id:
result = library_data.get_library_details(section_id=section_id)
status_message = ''
else:
result = None
status_message = 'An error occured.'
return serve_template(templatename="edit_library.html", title="Edit Library", data=result, status_message=status_message)
@cherrypy.expose @cherrypy.expose
def edit_library(self, section_id=None, **kwargs): def edit_library(self, section_id=None, **kwargs):
do_notify = kwargs.get('do_notify', 0) do_notify = kwargs.get('do_notify', 0)
@ -670,9 +696,9 @@ class WebInterface(object):
if 'reference_id' in kwargs: if 'reference_id' in kwargs:
reference_id = kwargs.get('reference_id', "") reference_id = kwargs.get('reference_id', "")
custom_where.append(['session_history.reference_id', reference_id]) custom_where.append(['session_history.reference_id', reference_id])
if 'library_id' in kwargs: if 'section_id' in kwargs:
library_id = kwargs.get('library_id', "") section_id = kwargs.get('section_id', "")
custom_where.append(['session_history_metadata.library_id', library_id]) custom_where.append(['session_history_metadata.library_id', section_id])
if 'media_type' in kwargs: if 'media_type' in kwargs:
media_type = kwargs.get('media_type', "") media_type = kwargs.get('media_type', "")
if media_type != 'all': if media_type != 'all':
@ -888,7 +914,7 @@ class WebInterface(object):
return None return None
@cherrypy.expose @cherrypy.expose
def info(self, library_id=None, rating_key=None, source=None, **kwargs): def info(self, rating_key=None, source=None, **kwargs):
# Make sure our library sections are up to date. # Make sure our library sections are up to date.
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
data_factory.update_library_sections() data_factory.update_library_sections()
@ -905,11 +931,6 @@ class WebInterface(object):
if source == 'history': if source == 'history':
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
metadata = data_factory.get_metadata_details(rating_key=rating_key) metadata = data_factory.get_metadata_details(rating_key=rating_key)
elif library_id:
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_library_metadata_details(library_id=library_id)
if result:
metadata = result['metadata']
else: else:
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_metadata_details(rating_key=rating_key) result = pms_connect.get_metadata_details(rating_key=rating_key)
@ -972,6 +993,20 @@ class WebInterface(object):
return serve_template(templatename="user_recently_watched.html", data=None, return serve_template(templatename="user_recently_watched.html", data=None,
title="Recently Watched") title="Recently Watched")
@cherrypy.expose
def get_library_recently_watched(self, library_id=None, limit='10', **kwargs):
data_factory = datafactory.DataFactory()
result = data_factory.get_recently_watched(library_id=library_id, limit=limit)
if result:
return serve_template(templatename="user_recently_watched.html", data=result,
title="Recently Watched")
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="user_recently_watched.html", data=None,
title="Recently Watched")
@cherrypy.expose @cherrypy.expose
def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs): def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs):
@ -984,6 +1019,18 @@ class WebInterface(object):
logger.warn('Unable to retrieve data.') logger.warn('Unable to retrieve data.')
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose
def get_library_watch_time_stats(self, library_id=None, **kwargs):
library_data = libraries.Libraries()
result = library_data.get_library_watch_time_stats(library_id=library_id)
if result:
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats")
@cherrypy.expose @cherrypy.expose
def get_user_player_stats(self, user=None, user_id=None, **kwargs): def get_user_player_stats(self, user=None, user_id=None, **kwargs):
@ -997,6 +1044,18 @@ class WebInterface(object):
logger.warn('Unable to retrieve data.') logger.warn('Unable to retrieve data.')
return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats") return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats")
@cherrypy.expose
def get_library_user_stats(self, library_id=None, **kwargs):
library_data = libraries.Libraries()
result = library_data.get_library_user_stats(library_id=library_id)
if result:
return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats")
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats")
@cherrypy.expose @cherrypy.expose
def get_item_children(self, rating_key='', **kwargs): def get_item_children(self, rating_key='', **kwargs):
@ -1594,11 +1653,11 @@ class WebInterface(object):
return json.dumps({'message': 'no data received'}) return json.dumps({'message': 'no data received'})
@cherrypy.expose @cherrypy.expose
def delete_all_library_history(self, library_id, **kwargs): def delete_all_library_history(self, section_id, **kwargs):
library_data = libraries.Libraries() library_data = libraries.Libraries()
if library_id: if section_id:
delete_row = library_data.delete_all_library_history(library_id=library_id) delete_row = library_data.delete_all_library_history(section_id=section_id)
if delete_row: if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'