This commit is contained in:
alexandrupaulpopa 2015-08-19 18:22:20 +00:00
commit 009168eebd
29 changed files with 689 additions and 200 deletions

View file

@ -1,13 +1,15 @@
# Changelog # Changelog
## v1.0 (2015-08-11) ## v1.1.2 (2015-08-16)
* First release * Fix bug where user refresh would fail under certain circumstances.
## v1.0.1 (2015-08-13) ## v1.1.1 (2015-08-15)
* Allow SSL certificate check override for certain systems with bad CA stores. * Added Most watched movie for home stats. Thanks @jroyal.
* Fix typo on graphs page causing date selection to break on Safari. * Added TV show title to recently added text. Thanks @jroyal.
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
* Fix bug where custom avatar URL would get reset on every user refresh.
## v1.1.0 (2015-08-15) ## v1.1.0 (2015-08-15)
@ -24,13 +26,11 @@
* Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close. * Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close.
* Fix some styling niggles. * Fix some styling niggles.
## v1.1.1 (2015-08-15) ## v1.0.1 (2015-08-13)
* Added Most watched movie for home stats. Thanks @jroyal. * Allow SSL certificate check override for certain systems with bad CA stores.
* Added TV show title to recently added text. Thanks @jroyal. * Fix typo on graphs page causing date selection to break on Safari.
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
* Fix bug where custom avatar URL would get reset on every user refresh.
## v1.1.2 (2015-08-16) ## v1.0 (2015-08-11)
* Fix bug where user refresh would fail under certain circumstances. * First release

View file

@ -6,7 +6,7 @@ This project is based on code from Headphones (https://github.com/rembo10/headph
* plexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program * plexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
If you'd like to buy me a beer, hit the donate button below. If you'd like to buy me a beer, hit the donate button below. All donations go to the project maintainer (primarily for the procurement of liquid refreshment).
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9HZK9BDJLKT6) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9HZK9BDJLKT6)
@ -34,6 +34,14 @@ If you'd like to buy me a beer, hit the donate button below.
* stream type (direct, transcoded) * stream type (direct, transcoded)
* video type & resolution * video type & resolution
* audio type & channel count. * audio type & channel count.
* Top statistics on home page with configurable duration and measurement metric:
* Most watched TV
* Most popular TV
* Most watched Movie
* Most popular Movie
* Most active user
* Most active platform
* Recently added media and how long ago it was added * Recently added media and how long ago it was added
@ -41,42 +49,49 @@ If you'd like to buy me a beer, hit the donate button below.
* date * date
* user * user
* platform * platform
* ip address (if enabled in plexWatch) * ip address
* title * title
* stream information details * stream information details
* start time * start time
* paused duration length * paused duration length
* stop time * stop time
* duration length * duration length
* percentage completed * watched progress
* show/hide columns
* delete mode - allows deletion of specific history items
* Full user list with general information and comparison stats * Full user list with general information and comparison stats
* Individual user information * Individual user information
- username and gravatar (if available) * username and gravatar (if available)
- daily, weekly, monthly, all time stats for play count and duration length * daily, weekly, monthly, all time stats for play count and duration length
- individual platform stats for each user * individual platform stats for each user
- public ip address history with last seen date and geo tag location * public ip address history with last seen date and geo tag location
- recently watched content * recently watched content
- watching history * watching history
- synced items * synced items
* assign users custom friendly names within PlexPy
* assign users custom avatar URL within PlexPy
* disable history logging per user
* disable notifications per user
* option to purge all history per user.
* Rich analytics presented using Highcharts graphing * Rich analytics presented using Highcharts graphing
- user-selectable time periods of 30, 90 or 365 days * user-selectable time periods of 30, 90 or 365 days
- daily watch count and duration * daily watch count and duration
- totals by day of week and hours of the day * totals by day of week and hours of the day
- totals by top 10 platform * totals by top 10 platform
- totals by top 10 users * totals by top 10 users
- detailed breakdown by transcode decision * detailed breakdown by transcode decision
- source and stream resolutions * source and stream resolutions
- transcode decision counts by user and platform * transcode decision counts by user and platform
- total monthly counts * total monthly counts
* Content information pages * Content information pages
- movies (includes watching history) * movies (includes watching history)
- tv shows (includes watching history) * tv shows (includes watching history)
- tv seasons * tv seasons
- tv episodes (includes watching history) * tv episodes (includes watching history)
* Full sync list data on all users syncing items from your library * Full sync list data on all users syncing items from your library

View file

@ -75,6 +75,7 @@ ul.ColVis_collection {
background-color: #444; background-color: #444;
overflow: hidden; overflow: hidden;
z-index: 2002; z-index: 2002;
border-radius: 4px;
} }
ul.ColVis_collection li { ul.ColVis_collection li {

View file

@ -253,13 +253,7 @@ fieldset[disabled] .btn-bright.active {
.modal-body table { .modal-body table {
color: #999; color: #999;
} }
.modal-body ul {
list-style: none;
-webkit-padding-start: 0px;
margin: 0;
}
.modal-body li { .modal-body li {
list-style: none;
margin-top: 7px; margin-top: 7px;
margin-left: 4px; margin-left: 4px;
color: #aaa; color: #aaa;
@ -270,6 +264,9 @@ fieldset[disabled] .btn-bright.active {
.modal-body i { .modal-body i {
color: #F9AA03; color: #F9AA03;
} }
.modal-body i.fa {
color: #fff;
}
.modal-body strong { .modal-body strong {
color: #F9AA03; color: #F9AA03;
} }
@ -407,39 +404,31 @@ input[type="color"],
} }
.poster { .poster {
float: left; float: left;
min-height: 232px; min-height: 225px;
min-width: 155px; min-width: 150px;
margin-bottom: 8px;
position: relative; position: relative;
} }
.poster-face img { .poster-face {
bottom: 0; background-position: center;
overflow: hidden; background-size: cover;
height: 225px; height: 225px;
width: 153px; width: 150px;
position: relative;
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);
}
.cover-face {
background-position: center;
background-size: cover;
height: 150px;
width: 150px;
position: absolute; position: absolute;
bottom: 5px;
left: 0;
border: 1px solid rgba(128, 128, 128, 0.3);
}
.poster-face img:hover {
webkit-box-shadow: 0 0 0 2px #e9a049;
-moz-box-shadow: 0 0 0 2px #e9a049;
box-shadow: 0 0 0 2px #e9a049;
}
.cover-face img {
bottom: 0; bottom: 0;
overflow: hidden; webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
height: 153px; -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
width: 153px; box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
border: 1px solid rgba(128, 128, 128, 0.3);
position: absolute;
bottom: 5px;
left: 0;
}
.cover-face img:hover {
webkit-box-shadow: 0 0 0 2px #e9a049;
-moz-box-shadow: 0 0 0 2px #e9a049;
box-shadow: 0 0 0 2px #e9a049;
} }
.users-poster-face img { .users-poster-face img {
bottom: 0; bottom: 0;
@ -630,6 +619,11 @@ input[type="color"],
} }
.dashboard-recent-media-instance { .dashboard-recent-media-instance {
} }
.dashboard-recent-media-instance a:hover .poster-face {
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;
}
.dashboard-recent-media li { .dashboard-recent-media li {
margin-right: 27px; margin-right: 27px;
position: relative; position: relative;
@ -650,13 +644,14 @@ input[type="color"],
font-size: 13px; font-size: 13px;
margin: 0; margin: 0;
line-height: 15px; line-height: 15px;
font-weight: bold; font-weight: normal;
width: 153px; width: 153px;
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
clear: both; clear: both;
} }
.dashboard-recent-media-metacontainer text-muted { .dashboard-recent-media-metacontainer .text-muted {
padding-top: 5px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@ -1160,7 +1155,7 @@ input[type="color"],
position: relative; position: relative;
font-size: 13px; font-size: 13px;
line-height: 15px; line-height: 15px;
font-weight: bold; font-weight: normal;
width: 140px; width: 140px;
margin-left: 10px; margin-left: 10px;
} }

View file

@ -58,6 +58,12 @@ DOCUMENTATION :: END
</label> </label>
<p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p> <p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p>
</div> </div>
% if data['user_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 user. This is permanent!</p>
</div>
% endif
</fieldset> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -112,6 +118,21 @@ DOCUMENTATION :: END
}); });
% endif % endif
}); });
$("#delete-all-history").click(function() {
var r = confirm("Are you REALLY REALLY REALLY sure you want to delete all history for this user?");
if (r == true) {
$.ajax({
url: 'delete_all_user_history',
data: {user_id: '${data['user_id']}'},
cache: false,
async: true,
success: function(data) {
location.reload();
}
});
}
});
</script> </script>
% endif % endif

View file

@ -263,9 +263,35 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
var current_range = 30; // Save graph state to cookies
$('input[name=yaxis-options]').change(function() {
setCookie('graphType', $(this).val(), 365, '/');
});
$('input[name=date-options]').change(function() {
setCookie('graphDate', $(this).val(), 365, '/');
});
$('a[data-toggle=tab]').click(function() {
setCookie('graphTab', $(this).attr('href'), 365, '/');
});
// Initial values for graph if no saved state
var yaxis = 'plays'; var yaxis = 'plays';
var current_range = 30;
var current_tab = '#tabs-1'; var current_tab = '#tabs-1';
// Read saved graph state from cookies and set initial values
if(getCookie('graphType')) {
var yaxis = getCookie('graphType');
$('input[name=yaxis-options][value=' + yaxis + ']').prop('checked', true).trigger('click');
}
if(getCookie('graphDate')) {
var current_range = getCookie('graphDate');
$('input[name=date-options][value=' + current_range + ']').prop('checked', true).trigger('click');
}
if(getCookie('graphTab')) {
var current_tab = getCookie('graphTab');
$('a[data-toggle=tab][href=' + current_tab + ']').trigger('click');
}
function loadGraphsTab1(time_range, yaxis) { function loadGraphsTab1(time_range, yaxis) {
setGraphFormat(yaxis); setGraphFormat(yaxis);
@ -432,12 +458,8 @@
data: { y_axis: yaxis }, data: { y_axis: yaxis },
dataType: "json", dataType: "json",
success: function(data) { success: function(data) {
var dateArray = [];
for (var i = 0; i < data.categories.length; i++) {
dateArray.push(moment(data.categories[i], 'YYYY-MM').format('MMM YYYY'));
}
hc_plays_by_month_options.yAxis.min = 0; hc_plays_by_month_options.yAxis.min = 0;
hc_plays_by_month_options.xAxis.categories = dateArray; hc_plays_by_month_options.xAxis.categories = data.categories;
hc_plays_by_month_options.series = data.series; hc_plays_by_month_options.series = data.series;
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options); var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
} }

View file

@ -13,13 +13,16 @@
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-history"></i> History</span> <span><i class="fa fa-history"></i> History</span>
</div> </div>
<div class="colvis-button-bar hidden-xs"> <div class="button-bar">
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete mode</button>&nbsp
<div class="colvis-button-bar hidden-xs"></div>
</div> </div>
</div> </div>
<div class='table-card-back'> <div class='table-card-back'>
<table class="display" id="history_table" width="100%"> <table class="display" id="history_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align='left' id="delete_row">Delete</th>
<th align='left' id="time">Time</th> <th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th> <th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th> <th align='left' id="platform">Platform</th>
@ -29,7 +32,7 @@
<th align='left' id="paused_counter">Paused</th> <th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th> <th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th> <th align='left' id="percent_complete"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -60,8 +63,21 @@
} }
} }
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
}); });
</script> </script>

View file

@ -9,7 +9,8 @@ Variable names: data [array]
data[array_index] :: Usable parameters data[array_index] :: Usable parameters
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'popular_tv', 'top_user' or 'top_platform' data['stat_id'] Returns the name of the stat. Either 'top_tv', 'top_movies', 'popular_tv', 'popular_movies', 'top_user' or 'top_platform'
data['stat_type'] Returns the type of the stat. Either 'total_plays' or 'total_duration'
data['rows'] Returns an array containing stat data data['rows'] Returns an array containing stat data
data[array_index]['rows'] :: Usable parameters data[array_index]['rows'] :: Usable parameters
@ -21,10 +22,11 @@ grandparent_thumb Returns location of the item's thumbnail. Use with pms_i
rating_key Returns the unique identifier for the media item. rating_key Returns the unique identifier for the media item.
title Returns the title for the associated stat. title Returns the title for the associated stat.
== Only if 'stat_id' is 'top_tv' or 'top_user' or 'top_platform' == == Only if 'stat_id' is 'top_tv' or 'top_movies' or 'top_user' or 'top_platform' ==
total_plays Returns the count for the associated stat. total_plays Returns the count for the associated stat.
total_duration Returns the total duration for the associated stat.
== Only of 'stat_id' is 'popular_tv' == == Only of 'stat_id' is 'popular_tv' or 'popular_movies' ==
users_watched Returns the count for the associated stat. users_watched Returns the count for the associated stat.
== Only if 'stat_id' is 'top_user' == == Only if 'stat_id' is 'top_user' ==
@ -39,6 +41,22 @@ platform_type Returns the platform name for the associated stat.
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%doc>
<%!
from plexpy import helpers
# Human readable duration
def hd(minutes):
if int(minutes) > 60:
hours = int(helpers.cast_to_float(minutes) / 60)
minutes = int(helpers.cast_to_float(minutes) % hours)
if minutes > 0:
return "<h3>" + str(hours) + "</h3><p>hrs</p><h3>" + str(minutes) + "</h3><p>mins</p>"
else:
return "<h3>" + str(hours) + "</h3><p>hrs</p>"
else:
return "<h3>" + minutes + "</h3><p>mins</p>"
%>
% if data: % if data:
% if data[0]['rows'] or data[2]['rows']: % if data[0]['rows'] or data[2]['rows']:
<ul class="list-unstyled"> <ul class="list-unstyled">
@ -63,8 +81,12 @@ DOCUMENTATION :: END
</a></h5> </a></h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div> </div>
</li> </li>
</div> </div>
@ -113,8 +135,37 @@ DOCUMENTATION :: END
</a></h5> </a></h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div>
</li>
</div>
% elif a['stat_id'] == 'popular_movies' and a['rows']:
<div class="home-platforms-instance">
<li>
<span>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['thumb']:
<img class="home-platforms-instance-poster"
src="pms_image_proxy?img=${a['rows'][0]['thumb']}&width=162&height=240&fallback=poster">
% else:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Popular Movie</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['users_watched']}</h3>
<p> users</p>
</div> </div>
</li> </li>
</div> </div>
@ -149,8 +200,12 @@ DOCUMENTATION :: END
</h5> </h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div> </div>
</li> </li>
</div> </div>
@ -165,8 +220,12 @@ DOCUMENTATION :: END
<h5>${a['rows'][0]['platform_type']}</h5> <h5>${a['rows'][0]['platform_type']}</h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div> </div>
</li> </li>
</div> </div>

View file

@ -45,12 +45,12 @@
<script src="interfaces/default/js/moment-with-locale.js"></script> <script src="interfaces/default/js/moment-with-locale.js"></script>
<script> <script>
function getHomeStats(days) { function getHomeStats(days, plays) {
$.ajax({ $.ajax({
url: 'home_stats', url: 'home_stats',
cache: false, cache: false,
async: true, async: true,
data: {time_range: days}, data: {time_range: days, stat_type: plays},
complete: function(xhr, status) { complete: function(xhr, status) {
$("#home-stats").html(xhr.responseText); $("#home-stats").html(xhr.responseText);
} }
@ -110,7 +110,7 @@
}); });
}); });
getHomeStats(${config['home_stats_length']}); getHomeStats(${config['home_stats_length']}, ${config['home_stats_type']});
</script> </script>

View file

@ -173,6 +173,7 @@ DOCUMENTATION :: END
<table class="display" id="history_table" width="100%"> <table class="display" id="history_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th> <th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th> <th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th> <th align='left' id="platform">Platform</th>
@ -182,7 +183,7 @@ DOCUMENTATION :: END
<th align='left' id="paused_counter">Paused</th> <th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th> <th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th> <th align='left' id="percent_complete"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -254,9 +255,8 @@ DOCUMENTATION :: END
} }
} }
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').DataTable(history_table_options);
history_table.column(4).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
}); });
</script> </script>
@ -274,7 +274,7 @@ DOCUMENTATION :: END
} }
} }
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
}); });
</script> </script>

View file

@ -15,7 +15,7 @@
<div class="modal-body" id="modal-text"> <div class="modal-body" id="modal-text">
<div class="col-md-6"> <div class="col-md-6">
<h4><strong>Location Details</strong></h4> <h4><strong>Location Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>Country: <strong><span id="country"></span></strong></li> <li>Country: <strong><span id="country"></span></strong></li>
<li>City: <strong><span id="city"></span></strong></li> <li>City: <strong><span id="city"></span></strong></li>
<li>Region: <strong><span id="region"></span></strong></li> <li>Region: <strong><span id="region"></span></strong></li>
@ -26,7 +26,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h4><strong>Connection Details</strong></h4> <h4><strong>Connection Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>ISP: <strong><span id="isp"></span></strong></li> <li>ISP: <strong><span id="isp"></span></strong></li>
<li>Organization: <strong><span id="org"></span></strong></li> <li>Organization: <strong><span id="org"></span></strong></li>
<li>AS: <strong><span id="as"></span></strong></li> <li>AS: <strong><span id="as"></span></strong></li>

View file

@ -25,10 +25,11 @@ history_table_options = {
"processing": false, "processing": false,
"serverSide": true, "serverSide": true,
"pageLength": 25, "pageLength": 25,
"order": [ 0, 'desc'], "order": [ 1, 'desc'],
"autoWidth": false,
"columnDefs": [ "columnDefs": [
{ {
"targets": [0], "targets": [1],
"data":"date", "data":"date",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (rowData['stopped'] === null) { if (rowData['stopped'] === null) {
@ -41,7 +42,7 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [1], "targets": [2],
"data":"friendly_name", "data":"friendly_name",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -57,7 +58,7 @@ history_table_options = {
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [2], "targets": [3],
"data":"player", "data":"player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -67,7 +68,7 @@ history_table_options = {
"className": "modal-control no-wrap hidden-sm hidden-xs" "className": "modal-control no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [3], "targets": [4],
"data":"ip_address", "data":"ip_address",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData) { if (cellData) {
@ -87,7 +88,7 @@ history_table_options = {
"className": "no-wrap hidden-xs modal-control-ip" "className": "no-wrap hidden-xs modal-control-ip"
}, },
{ {
"targets": [4], "targets": [5],
"data":"full_title", "data":"full_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@ -106,7 +107,7 @@ history_table_options = {
} }
}, },
{ {
"targets": [5], "targets": [6],
"data":"started", "data":"started",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@ -119,7 +120,7 @@ history_table_options = {
"className": "no-wrap hidden-sm hidden-xs" "className": "no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [6], "targets": [7],
"data":"paused_counter", "data":"paused_counter",
"render": function ( data, type, full ) { "render": function ( data, type, full ) {
if (data !== null) { if (data !== null) {
@ -132,7 +133,7 @@ history_table_options = {
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [7], "targets": [8],
"data":"stopped", "data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@ -145,7 +146,7 @@ history_table_options = {
"className": "no-wrap hidden-md hidden-xs" "className": "no-wrap hidden-md hidden-xs"
}, },
{ {
"targets": [8], "targets": [9],
"data":"duration", "data":"duration",
"render": function ( data, type, full ) { "render": function ( data, type, full ) {
if (data !== null) { if (data !== null) {
@ -158,7 +159,7 @@ history_table_options = {
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [9], "targets": [10],
"data":"percent_complete", "data":"percent_complete",
"render": function ( data, type, full ) { "render": function ( data, type, full ) {
if (data > 80) { if (data > 80) {
@ -173,7 +174,17 @@ history_table_options = {
"orderable": false, "orderable": false,
"className": "no-wrap hidden-md hidden-xs", "className": "no-wrap hidden-md hidden-xs",
"width": "10px" "width": "10px"
} },
{
"targets": [0],
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<button class="btn btn-xs btn-danger" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o"></i> Delete</button>');
},
"className": "delete-control no-wrap hidden",
"searchable": false,
"orderable": false
},
], ],
"drawCallback": function (settings) { "drawCallback": function (settings) {
// Jump to top of page // Jump to top of page
@ -183,6 +194,11 @@ history_table_options = {
$('.info-modal').each(function() { $('.info-modal').each(function() {
$(this).tooltip(); $(this).tooltip();
}); });
if ($('#row-edit-mode').hasClass('active')) {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
}, },
"preDrawCallback": function(settings) { "preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>"; var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
@ -228,6 +244,24 @@ $('#history_table').on('click', 'td.modal-control-ip', function () {
}); });
} }
} }
getUserLocation(rowData['ip_address']); getUserLocation(rowData['ip_address']);
});
$('#history_table').on('click', 'td.delete-control', function () {
var tr = $(this).parents('tr');
var row = history_table.row( tr );
var rowData = row.data();
$(this).children("button").prop('disabled', true);
$(this).children("button").html('<i class="fa fa-spin fa-refresh"></i> Delete');
$.ajax({
url: 'delete_history_rows',
data: {row_id: rowData['id']},
async: true,
success: function(data) {
history_table.ajax.reload(null, false);
}
});
}); });

View file

@ -29,29 +29,32 @@ DOCUMENTATION :: END
% for item in data: % for item in data:
<div class="dashboard-recent-media-instance"> <div class="dashboard-recent-media-instance">
<li> <li>
<div class="poster"> % if item['type'] == 'season' or item['type'] == 'movie':
% if item['type'] == 'season' or item['type'] == 'movie': <a href="info?item_id=${item['rating_key']}">
<div class="poster-face"> <div class="poster">
<a href="info?item_id=${item['rating_key']}"> <div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
</a>
</div> </div>
% elif item['type'] == 'album': <div class="dashboard-recent-media-metacontainer">
<div class="cover-face"> % if item['type'] == 'season':
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover" class="cover-face"> <h3>${item['parent_title']}</h3>
</div> <h3>(${item['title']})</h3>
% endif % elif item['type'] == 'movie':
</div> <h3>${item['title']}</h3>
<div class="dashboard-recent-media-metacontainer"> <h3>(${item['year']})</h3>
% if item['type'] == 'season': % endif
<h3>${item['parent_title']} - ${item['title']}</h3> <div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
% elif item['type'] == 'album': </div>
<h3>${item['title']}</h3> </a>
% elif item['type'] == 'movie': % elif item['type'] == 'album':
<h3>${item['title']} (${item['year']})</h3> <div class="poster">
% endif <div class="cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div> </div>
</div> <div class="dashboard-recent-media-metacontainer">
<h3>${item['parent_title']}</h3>
<h3>${item['title']}</h3>
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
% endif
</li> </li>
</div> </div>
<script> <script>

View file

@ -1,7 +1,7 @@
<%inherit file="base.html"/> <%inherit file="base.html"/>
<%! <%!
import plexpy import plexpy
from plexpy import notifiers from plexpy import notifiers, common, versioncheck
available_notification_agents = notifiers.available_notification_agents() available_notification_agents = notifiers.available_notification_agents()
%> %>
@ -48,9 +48,11 @@ available_notification_agents = notifiers.available_notification_agents()
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate> <form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-1"> <div role="tabpanel" class="tab-pane active" id="tabs-1">
% if common.VERSION_NUMBER:
<div class="padded-header"> <div class="padded-header">
<h3>Software Updates</h3> <h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
</div> </div>
% endif
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates <input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
@ -58,7 +60,7 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p> <p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
</div> </div>
% if plexpy.CURRENT_VERSION: % if plexpy.CURRENT_VERSION:
<p>Current version: ${plexpy.CURRENT_VERSION}</p> <p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
% endif % endif
<div class="padded-header"> <div class="padded-header">
<h3>Display Settings</h3> <h3>Display Settings</h3>
@ -276,8 +278,12 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">If you have media indexing enabled on your server, use these on the activity pane.</p> <p class="help-block">If you have media indexing enabled on your server, use these on the activity pane.</p>
</div> </div>
<div class="padded-header">
<h3>Homepage Statistics</h3>
</div>
<div class="form-group"> <div class="form-group">
<label for="home_stats_length">Homepage Statistics Time Frame</label> <label for="home_stats_length">Time Frame</label>
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="home_stats_length" name="home_stats_length" value="${config['home_stats_length']}" size="3" data-parsley-min="0" data-parsley-trigger="change" required> <input type="text" class="form-control" data-parsley-type="integer" id="home_stats_length" name="home_stats_length" value="${config['home_stats_length']}" size="3" data-parsley-min="0" data-parsley-trigger="change" required>
@ -285,6 +291,12 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<p class="help-block">Specify the number of days for the statistics on the home page. Default is 30 days.</p> <p class="help-block">Specify the number of days for the statistics on the home page. Default is 30 days.</p>
</div> </div>
<div class="checkbox">
<label>
<input type="checkbox" id="home_stats_type" name="home_stats_type" value="1" ${config['home_stats_type']}> Use play duration
</label>
<p class="help-block">Use play duration instead of play count to generate statistics.</p>
</div>
<div class="padded-header"> <div class="padded-header">
<h3>Plex Logs</h3> <h3>Plex Logs</h3>
@ -810,10 +822,18 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{season_num}</strong></td> <td width="150"><strong>{season_num}</strong></td>
<td>The season number for the media item if item is episode.</td> <td>The season number for the media item if item is episode.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{season_num00}</strong></td>
<td>The two digit season number.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{episode_num}</strong></td> <td width="150"><strong>{episode_num}</strong></td>
<td>The episode number for the media item if item is episode.</td> <td>The episode number for the media item if item is episode.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{episode_num00}</strong></td>
<td>The two digit episode number.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{rating}</strong></td> <td width="150"><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</td> <td>The rating (out of 10) for the item.</td>
@ -875,6 +895,21 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
</div> </div>
</div> </div>
<div id="changelog-modal" class="modal fade" 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">Changelog</h4>
</div>
<div class="modal-body">
${versioncheck.read_changelog()}
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</div> </div>
</%def> </%def>
@ -1023,7 +1058,9 @@ $(document).ready(function() {
headers: {'Content-Type': 'application/xml; charset=utf-8', headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy', 'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': 'PlexPy', 'X-Plex-Product': 'PlexPy',
'X-Plex-Version': 'v0.1 dev', 'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}', 'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val()) 'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
}, },

View file

@ -55,7 +55,7 @@ DOCUMENTATION :: END
<div class="col-md-4"> <div class="col-md-4">
<h4><strong>Stream Details</strong></h4> <h4><strong>Stream Details</strong></h4>
<h5>Video</h5> <h5>Video</h5>
<ul> <ul class="list-unstyled">
% if data['transcode_video_dec'] != 'direct play': % if data['transcode_video_dec'] != 'direct play':
<li>Stream Type: <strong>${data['transcode_video_dec']}</strong></li> <li>Stream Type: <strong>${data['transcode_video_dec']}</strong></li>
<li>Video Resolution: <strong>${data['transcode_height']}p</strong></li> <li>Video Resolution: <strong>${data['transcode_height']}p</strong></li>
@ -75,7 +75,7 @@ DOCUMENTATION :: END
% endif % endif
</ul> </ul>
<h5>Audio</h5> <h5>Audio</h5>
<ul> <ul class="list-unstyled">
% if data['transcode_audio_dec'] != 'direct play': % if data['transcode_audio_dec'] != 'direct play':
<li>Stream Type: <strong>${data['transcode_audio_dec']}</strong></li> <li>Stream Type: <strong>${data['transcode_audio_dec']}</strong></li>
<li>Audio Codec: <strong>${data['transcode_audio_codec']}</strong></li> <li>Audio Codec: <strong>${data['transcode_audio_codec']}</strong></li>
@ -89,7 +89,7 @@ DOCUMENTATION :: END
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<h4><strong>Media Source Details</strong></h4> <h4><strong>Media Source Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>Container: <strong>${data['container']}</strong></li> <li>Container: <strong>${data['container']}</strong></li>
<li>Resolution: <strong>${data['height']}p</strong></li> <li>Resolution: <strong>${data['height']}p</strong></li>
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li> <li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
@ -97,7 +97,7 @@ DOCUMENTATION :: END
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<h4><strong>Video Source Details</strong></h4> <h4><strong>Video Source Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>Width: <strong>${data['width']}</strong></li> <li>Width: <strong>${data['width']}</strong></li>
<li>Height: <strong>${data['height']}</strong></li> <li>Height: <strong>${data['height']}</strong></li>
<li>Aspect Ratio: <strong>${data['aspect_ratio']}</strong></li> <li>Aspect Ratio: <strong>${data['aspect_ratio']}</strong></li>
@ -105,7 +105,7 @@ DOCUMENTATION :: END
<li>Video Codec: <strong>${data['video_codec']}</strong></li> <li>Video Codec: <strong>${data['video_codec']}</strong></li>
</ul> </ul>
<h4><strong>Audio Source Details</strong></h4> <h4><strong>Audio Source Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>Audio Codec: <strong>${data['audio_codec']}</strong></li> <li>Audio Codec: <strong>${data['audio_codec']}</strong></li>
<li>Audio Channels: <strong>${data['audio_channels']}</strong></li> <li>Audio Channels: <strong>${data['audio_channels']}</strong></li>
</ul> </ul>

View file

@ -58,7 +58,7 @@
} }
sync_table = $('#sync_table').DataTable(sync_table_options); sync_table = $('#sync_table').DataTable(sync_table_options);
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } ); var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis.button() ).appendTo('div.colvis-button-bar'); $( colvis.button() ).appendTo('div.colvis-button-bar');
}); });
</script> </script>

View file

@ -140,8 +140,6 @@ from plexpy import helpers
</thead> </thead>
</table> </table>
</div> </div>
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div> </div>
</div> </div>
</div> </div>
@ -156,13 +154,17 @@ from plexpy import helpers
<span class="set-username">${data['friendly_name']}</span> <span class="set-username">${data['friendly_name']}</span>
</strong></span> </strong></span>
</div> </div>
<div class="colvis-button-bar hidden-xs" id="button-bar-history"> <div class="button-bar">
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete Mode</button>&nbsp
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
</div>
</div> </div>
</div> </div>
<div class="table-card-back"> <div class="table-card-back">
<table class="display" id="history_table" width="100%"> <table class="display" id="history_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th> <th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th> <th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th> <th align='left' id="platform">Platform</th>
@ -172,7 +174,7 @@ from plexpy import helpers
<th align='left' id="paused_counter">Paused</th> <th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th> <th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th> <th align='left' id="percent_complete"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -222,6 +224,8 @@ from plexpy import helpers
</div> </div>
</div> </div>
</div> </div>
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div> </div>
<footer></footer> <footer></footer>
</%def> </%def>
@ -289,9 +293,9 @@ from plexpy import helpers
} }
} }
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').DataTable(history_table_options);
history_table.column(1).visible(false); history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
}); });
@ -322,7 +326,7 @@ from plexpy import helpers
sync_table = $('#sync_table').DataTable(sync_table_options); sync_table = $('#sync_table').DataTable(sync_table_options);
history_table.column(1).visible(false); history_table.column(1).visible(false);
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } ); var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"> Select columns</i>', buttonClass: 'btn btn-dark' } );
$( colvis_sync.button() ).appendTo('#button-bar-sync'); $( colvis_sync.button() ).appendTo('#button-bar-sync');
}); });
@ -339,6 +343,19 @@ from plexpy import helpers
} }
}); });
}); });
// Delete mode button
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
}); });
</script> </script>
</%def> </%def>

View file

@ -18,6 +18,7 @@ time Returns the last watched time of the media.
title Returns the name of the movie or episode. title Returns the name of the movie or episode.
== Only if 'type' is 'episode == == Only if 'type' is 'episode ==
parent_title Returns the name of the TV Show a season belongs too.
parent_index Returns the season number. parent_index Returns the season number.
index Returns the episode number. index Returns the episode number.
@ -33,21 +34,22 @@ DOCUMENTATION :: END
% for item in data: % for item in data:
<div class="dashboard-recent-media-instance"> <div class="dashboard-recent-media-instance">
<li> <li>
<div class="poster"> <a href="info?source=history&item_id=${item['row_id']}">
<div class="poster-face"> <div class="poster">
<a href="info?source=history&item_id=${item['row_id']}"> <div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
</a>
</div> </div>
</div> <div class="dashboard-recent-media-metacontainer">
<div class="dashboard-recent-media-metacontainer"> % if item['type'] == 'episode':
% if item['type'] == 'episode': <h3>${item['parent_title']}</h3>
<h3>Season ${item['parentIndex']}, Episode ${item['index']}</h3> <h3>${item['title']}</h3>
% elif item['type'] == 'movie': <h3>(Season ${item['parent_index']}, Episode ${item['index']})</h3>
<h3>${item['title']} (${item['year']})</h3> % elif item['type'] == 'movie':
% endif <h3>${item['title']}</h3>
<div class="text-muted" id="time-${item['time']}">${item['time']}</div> <h3>(${item['year']})</h3>
</div> % endif
<div class="text-muted" id="time-${item['time']}">${item['time']}</div>
</div>
</a>
</li> </li>
</div> </div>
<script> <script>

View file

@ -1,6 +1,6 @@
<% <%
import plexpy import plexpy
from plexpy import version from plexpy import common
%> %>
<!doctype html> <!doctype html>
@ -355,7 +355,9 @@ from plexpy import version
headers: {'Content-Type': 'application/xml; charset=utf-8', headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy', 'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': 'PlexPy', 'X-Plex-Product': 'PlexPy',
'X-Plex-Version': 'v0.1 dev', 'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}', 'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val()) 'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
}, },

View file

@ -1,4 +1,4 @@
# PlexPy - Automatic music downloader for SABnzbd # PlexPy
# #
# Service Unit file for systemd system manager # Service Unit file for systemd system manager
# #
@ -53,7 +53,7 @@
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode) # graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit] [Unit]
Description=PlexPy - Automatic music downloader for SABnzbd Description=PlexPy
[Service] [Service]
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet

80
init-scripts/init.freenas Normal file
View file

@ -0,0 +1,80 @@
#!/bin/sh
#
# PROVIDE: plexpy
# REQUIRE: DAEMON sabnzbd
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# plexpy_enable (bool): Set to NO by default.
# Set it to YES to enable it.
# plexpy_user: The user account PlexPy daemon runs as what
# you want it to be. It uses '_sabnzbd' user by
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.
# Default is plexpy.pid in plexpy_dir.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
name="plexpy"
rcvar=${name}_enable
load_rc_config ${name}
: ${plexpy_enable:="NO"}
: ${plexpy_user:="_sabnzbd"}
: ${plexpy_dir:="/usr/local/share/plexpy"}
: ${plexpy_chdir:="${plexpy_dir}"}
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="/usr/sbin/daemon"
command_args="-f -p ${plexpy_pid} python2 ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
echo "Oops, you should be root before running this!"
exit 1
fi
verify_plexpy_pid() {
# Make sure the pid corresponds to the PlexPy process.
if [ -f ${plexpy_pid} ]; then
pid=`cat ${plexpy_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
return $?
else
return 0
fi
}
# Try to stop PlexPy cleanly by calling shutdown over http.
plexpy_stop() {
echo "Stopping $name."
verify_plexpy_pid
if [ -n "${pid}" ]; then
kill ${pid}
wait_for_pids ${pid}
echo "Stopped."
fi
}
plexpy_status() {
verify_plexpy_pid
if [ -n "${pid}" ]; then
echo "$name is running as ${pid}."
else
echo "$name is not running."
fi
}
run_rc_command "$1"

View file

@ -1,4 +1,4 @@
# plexpy - Automatic music downloader # plexpy
# #
# This is a session/user job. Install this file into /usr/share/upstart/sessions # This is a session/user job. Install this file into /usr/share/upstart/sessions
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if # if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if

View file

@ -19,14 +19,17 @@ Created on Aug 1, 2011
@author: Michael @author: Michael
''' '''
import platform import platform
import operator
import os
import re
from plexpy import version from plexpy import version
# Identify Our Application # Identify Our Application
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')' USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
' ' + platform.release() + ')'
PLATFORM = platform.system()
PLATFORM_VERSION = platform.release()
BRANCH = version.PLEXPY_VERSION
VERSION_NUMBER = version.PLEXPY_RELEASE_VERSION
# Notification Types # Notification Types
NOTIFY_STARTED = 1 NOTIFY_STARTED = 1

View file

@ -83,6 +83,7 @@ _CONFIG_DEFINITIONS = {
'GROWL_ON_BUFFER': (int, 'Growl', 0), 'GROWL_ON_BUFFER': (int, 'Growl', 0),
'GROWL_ON_WATCHED': (int, 'Growl', 0), 'GROWL_ON_WATCHED': (int, 'Growl', 0),
'HOME_STATS_LENGTH': (int, 'General', 30), 'HOME_STATS_LENGTH': (int, 'General', 30),
'HOME_STATS_TYPE': (int, 'General', 0),
'HTTPS_CERT': (str, 'General', ''), 'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''), 'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'), 'HTTP_HOST': (str, 'General', '0.0.0.0'),

View file

@ -110,14 +110,16 @@ class DataFactory(object):
return dict return dict
def get_home_stats(self, time_range='30'): def get_home_stats(self, time_range='30', stat_type='0'):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
time_range = '30' time_range = '30'
sort_type = 'total_plays' if stat_type == '0' else 'total_duration'
# This actually determines the output order in the home page # This actually determines the output order in the home page
stats_queries = ["top_tv", "popular_tv", "top_movies", "top_users", "top_platforms"] stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms"]
home_stats = [] home_stats = []
for stat in stats_queries: for stat in stats_queries:
@ -127,6 +129,10 @@ class DataFactory(object):
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.grandparent_title, ' \ 'session_history_metadata.grandparent_title, ' \
'COUNT(session_history_metadata.grandparent_title) as total_plays, ' \ 'COUNT(session_history_metadata.grandparent_title) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'session_history_metadata.grandparent_rating_key, ' \ 'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch,' \ 'MAX(session_history.started) as last_watch,' \
'session_history_metadata.grandparent_thumb ' \ 'session_history_metadata.grandparent_thumb ' \
@ -136,7 +142,7 @@ class DataFactory(object):
'>= datetime("now", "-%s days", "localtime") ' \ '>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "episode" ' \ 'AND session_history_metadata.media_type = "episode" ' \
'GROUP BY session_history_metadata.grandparent_title ' \ 'GROUP BY session_history_metadata.grandparent_title ' \
'ORDER BY total_plays DESC LIMIT 10' % time_range 'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query.")
@ -145,10 +151,11 @@ class DataFactory(object):
for item in result: for item in result:
row = {'title': item[1], row = {'title': item[1],
'total_plays': item[2], 'total_plays': item[2],
'total_duration': item[3],
'users_watched': '', 'users_watched': '',
'rating_key': item[3], 'rating_key': item[4],
'last_play': item[4], 'last_play': item[5],
'grandparent_thumb': item[5], 'grandparent_thumb': item[6],
'thumb': '', 'thumb': '',
'user': '', 'user': '',
'friendly_name': '', 'friendly_name': '',
@ -159,6 +166,7 @@ class DataFactory(object):
top_tv.append(row) top_tv.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_tv}) 'rows': top_tv})
elif 'top_movies' in stat: elif 'top_movies' in stat:
@ -167,6 +175,10 @@ class DataFactory(object):
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \ 'session_history_metadata.full_title, ' \
'COUNT(session_history_metadata.full_title) as total_plays, ' \ 'COUNT(session_history_metadata.full_title) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'session_history_metadata.rating_key, ' \ 'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch,' \ 'MAX(session_history.started) as last_watch,' \
'session_history_metadata.thumb ' \ 'session_history_metadata.thumb ' \
@ -176,7 +188,7 @@ class DataFactory(object):
'>= datetime("now", "-%s days", "localtime") ' \ '>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "movie" ' \ 'AND session_history_metadata.media_type = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \ 'GROUP BY session_history_metadata.full_title ' \
'ORDER BY total_plays DESC LIMIT 10' % time_range 'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query.")
@ -185,11 +197,12 @@ class DataFactory(object):
for item in result: for item in result:
row = {'title': item[1], row = {'title': item[1],
'total_plays': item[2], 'total_plays': item[2],
'total_duration': item[3],
'users_watched': '', 'users_watched': '',
'rating_key': item[3], 'rating_key': item[4],
'last_play': item[4], 'last_play': item[5],
'grandparent_thumb': '', 'grandparent_thumb': '',
'thumb': item[5], 'thumb': item[6],
'user': '', 'user': '',
'friendly_name': '', 'friendly_name': '',
'platform_type': '', 'platform_type': '',
@ -199,6 +212,7 @@ class DataFactory(object):
top_movies.append(row) top_movies.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_movies}) 'rows': top_movies})
elif 'popular_tv' in stat: elif 'popular_tv' in stat:
@ -243,6 +257,48 @@ class DataFactory(object):
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'rows': popular_tv}) 'rows': popular_tv})
elif 'popular_movies' in stat:
popular_movies = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \
'COUNT(DISTINCT session_history.user_id) as users_watched, ' \
'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch, ' \
'COUNT(session_history.id) as total_plays, ' \
'session_history_metadata.thumb ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \
'ORDER BY users_watched DESC, total_plays DESC ' \
'LIMIT 10' % time_range
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
row = {'title': item[1],
'users_watched': item[2],
'rating_key': item[3],
'last_play': item[4],
'total_plays': item[5],
'grandparent_thumb': '',
'thumb': item[6],
'user': '',
'friendly_name': '',
'platform_type': '',
'platform': '',
'row_id': item[0]
}
popular_movies.append(row)
home_stats.append({'stat_id': stat,
'rows': popular_movies})
elif 'top_users' in stat: elif 'top_users' in stat:
top_users = [] top_users = []
try: try:
@ -250,6 +306,10 @@ class DataFactory(object):
'(case when users.friendly_name is null then session_history.user else ' \ '(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \ 'users.friendly_name end) as friendly_name,' \
'COUNT(session_history.id) as total_plays, ' \ 'COUNT(session_history.id) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'MAX(session_history.started) as last_watch, ' \ 'MAX(session_history.started) as last_watch, ' \
'users.custom_avatar_url as thumb, ' \ 'users.custom_avatar_url as thumb, ' \
'users.user_id ' \ 'users.user_id ' \
@ -259,23 +319,24 @@ class DataFactory(object):
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") '\ 'datetime("now", "-%s days", "localtime") '\
'GROUP BY session_history.user_id ' \ 'GROUP BY session_history.user_id ' \
'ORDER BY total_plays DESC LIMIT 10' % time_range 'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query.")
return None return None
for item in result: for item in result:
if not item[4] or item[4] == '': if not item[5] or item[5] == '':
user_thumb = common.DEFAULT_USER_THUMB user_thumb = common.DEFAULT_USER_THUMB
else: else:
user_thumb = item[4] user_thumb = item[5]
row = {'user': item[0], row = {'user': item[0],
'user_id': item[5], 'user_id': item[6],
'friendly_name': item[1], 'friendly_name': item[1],
'total_plays': item[2], 'total_plays': item[2],
'last_play': item[3], 'total_duration': item[3],
'last_play': item[4],
'thumb': user_thumb, 'thumb': user_thumb,
'grandparent_thumb': '', 'grandparent_thumb': '',
'users_watched': '', 'users_watched': '',
@ -288,6 +349,7 @@ class DataFactory(object):
top_users.append(row) top_users.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_users}) 'rows': top_users})
elif 'top_platforms' in stat: elif 'top_platforms' in stat:
@ -296,6 +358,10 @@ class DataFactory(object):
try: try:
query = 'SELECT session_history.platform, ' \ query = 'SELECT session_history.platform, ' \
'COUNT(session_history.id) as total_plays, ' \ 'COUNT(session_history.id) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'MAX(session_history.started) as last_watch ' \ 'MAX(session_history.started) as last_watch ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
@ -310,7 +376,8 @@ class DataFactory(object):
for item in result: for item in result:
row = {'platform': item[0], row = {'platform': item[0],
'total_plays': item[1], 'total_plays': item[1],
'last_play': item[2], 'total_duration': item[2],
'last_play': item[3],
'platform_type': item[0], 'platform_type': item[0],
'title': '', 'title': '',
'thumb': '', 'thumb': '',
@ -324,6 +391,7 @@ class DataFactory(object):
top_platform.append(row) top_platform.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_platform}) 'rows': top_platform})
return home_stats return home_stats
@ -381,21 +449,21 @@ class DataFactory(object):
try: try:
if user_id: if user_id:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \ query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \ 'grandparent_title, thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata ' \ 'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user_id = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?' 'WHERE user_id = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user_id, limit]) result = monitor_db.select(query, args=[user_id, limit])
elif user: elif user:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \ query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \ 'grandparent_title, thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata ' \ 'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?' 'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user, limit]) result = monitor_db.select(query, args=[user, limit])
else: else:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \ query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \ 'grandparent_title, thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata WHERE session_history.media_type != "track"' \ 'FROM session_history_metadata WHERE session_history.media_type != "track"' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'ORDER BY started DESC LIMIT ?' 'ORDER BY started DESC LIMIT ?'
@ -406,20 +474,21 @@ class DataFactory(object):
for row in result: for row in result:
if row[1] == 'episode': if row[1] == 'episode':
thumb = row[5] thumb = row[6]
else: else:
thumb = row[4] thumb = row[5]
recent_output = {'row_id': row[0], recent_output = {'row_id': row[0],
'type': row[1], 'type': row[1],
'rating_key': row[2], 'rating_key': row[2],
'title': row[3], 'title': row[3],
'parent_title': row[4],
'thumb': thumb, 'thumb': thumb,
'index': row[6], 'index': row[7],
'parentIndex': row[7], 'parent_index': row[8],
'year': row[8], 'year': row[9],
'time': row[9], 'time': row[10],
'user': row[10] 'user': row[11]
} }
recently_watched.append(recent_output) recently_watched.append(recent_output)
@ -474,4 +543,48 @@ class DataFactory(object):
'actors': actors 'actors': actors
} }
return metadata return metadata
def delete_session_history_rows(self, row_id=None):
monitor_db = database.MonitorDatabase()
if row_id.isdigit():
logger.info(u"PlexPy DataFactory :: Deleting row id %s from the session history database." % row_id)
session_history_del = \
monitor_db.action('DELETE FROM session_history WHERE id = ?', [row_id])
session_history_media_info_del = \
monitor_db.action('DELETE FROM session_history_media_info WHERE id = ?', [row_id])
session_history_metadata_del = \
monitor_db.action('DELETE FROM session_history_metadata WHERE id = ?', [row_id])
return 'Deleted rows %s.' % row_id
else:
return 'Unable to delete rows. Input row not valid.'
def delete_all_user_history(self, user_id=None):
monitor_db = database.MonitorDatabase()
if user_id.isdigit():
logger.info(u"PlexPy DataFactory :: Deleting all history for user id %s from database." % user_id)
session_history_media_info_del = \
monitor_db.action('DELETE FROM '
'session_history_media_info '
'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id '
'FROM session_history_media_info '
'JOIN session_history ON session_history_media_info.id = session_history.id '
'WHERE session_history.user_id = ?)', [user_id])
session_history_metadata_del = \
monitor_db.action('DELETE FROM '
'session_history_metadata '
'WHERE session_history_metadata.id IN (SELECT session_history_metadata.id '
'FROM session_history_metadata '
'JOIN session_history ON session_history_metadata.id = session_history.id '
'WHERE session_history.user_id = ?)', [user_id])
session_history_del = \
monitor_db.action('DELETE FROM '
'session_history '
'WHERE session_history.user_id = ?', [user_id])
return 'Deleted all items for user_id %s.' % user_id
else:
return 'Unable to delete items. Input user_id not valid.'

View file

@ -317,7 +317,9 @@ def build_notify_text(session, state):
'content_rating': item_metadata['content_rating'], 'content_rating': item_metadata['content_rating'],
'summary': item_metadata['summary'], 'summary': item_metadata['summary'],
'season_num': item_metadata['parent_index'], 'season_num': item_metadata['parent_index'],
'season_num00': item_metadata['parent_index'].zfill(2),
'episode_num': item_metadata['index'], 'episode_num': item_metadata['index'],
'episode_num00': item_metadata['index'].zfill(2),
'album_name': item_metadata['parent_title'], 'album_name': item_metadata['parent_title'],
'rating': item_metadata['rating'], 'rating': item_metadata['rating'],
'duration': duration, 'duration': duration,

View file

@ -1 +1,2 @@
PLEXPY_VERSION = "master" PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.1.2"

View file

@ -241,3 +241,37 @@ def update():
e e
) )
return return
def read_changelog():
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
try:
logfile = open(changelog_file, "r")
except IOError, e:
logger.error('PlexPy Version Checker :: Unable to open changelog file. %s' % e)
return None
if logfile:
output = ''
lines = logfile.readlines()
previous_line = ''
for line in lines:
if line[:2] == '# ':
output += '<h3>' + line[2:] + '</h3>'
elif line[:3] == '## ':
output += '<h4>' + line[3:] + '</h4>'
elif line[:2] == '* ' and previous_line.strip() == '':
output += '<ul><li>' + line[2:] + '</li>'
elif line[:2] == '* ':
output += '<li>' + line[2:] + '</li>'
elif line.strip() == '' and previous_line[:2] == '* ':
output += '</ul></br>'
else:
output += line + '</br>'
previous_line = line
return output
else:
return '<h4>No changelog data</h4>'

View file

@ -65,7 +65,8 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def home(self): def home(self):
config = { config = {
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE
} }
return serve_template(templatename="index.html", title="Home", config=config) return serve_template(templatename="index.html", title="Home", config=config)
@ -118,9 +119,9 @@ class WebInterface(object):
return json.dumps(formats) return json.dumps(formats)
@cherrypy.expose @cherrypy.expose
def home_stats(self, time_range='30', **kwargs): def home_stats(self, time_range='30', stat_type='0', **kwargs):
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
stats_data = data_factory.get_home_stats(time_range=time_range) stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type)
return serve_template(templatename="home_stats.html", title="Stats", data=stats_data) return serve_template(templatename="home_stats.html", title="Stats", data=stats_data)
@ -451,6 +452,7 @@ class WebInterface(object):
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT, "notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT, "notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE),
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT "buffer_wait": plexpy.CONFIG.BUFFER_WAIT
} }
@ -473,7 +475,7 @@ class WebInterface(object):
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start", "tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start",
"tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop", "tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop",
"tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup", "tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup",
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote" "ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type"
] ]
for checked_config in checked_configs: for checked_config in checked_configs:
if checked_config not in kwargs: if checked_config not in kwargs:
@ -1256,3 +1258,32 @@ class WebInterface(object):
return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers", return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers",
data=this_agent) data=this_agent)
@cherrypy.expose
def delete_history_rows(self, row_id, **kwargs):
data_factory = datafactory.DataFactory()
if row_id:
delete_row = data_factory.delete_session_history_rows(row_id=row_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})
@cherrypy.expose
def delete_all_user_history(self, user_id, **kwargs):
data_factory = datafactory.DataFactory()
if user_id:
delete_row = data_factory.delete_all_user_history(user_id=user_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})