Compare commits

...

42 Commits

Author SHA1 Message Date
JonnyWong16
ab34a74210 v2.0.20-beta 2018-02-24 09:22:47 -08:00
JonnyWong16
cfa6de4d91 Remove content group 2018-02-24 09:02:46 -08:00
JonnyWong16
a5608c7a1e Revert to png for logos 2018-02-22 19:30:10 -08:00
JonnyWong16
88a7b52e51 Add content group metric dev/production 2018-02-22 12:39:32 -08:00
JonnyWong16
e444bad4de Switch metric dimensions 2018-02-22 12:28:22 -08:00
JonnyWong16
5403b0b547 Install or update event 2018-02-22 11:41:22 -08:00
JonnyWong16
51b5e615f5 Add some more system metrics 2018-02-22 09:20:58 -08:00
JonnyWong16
700547b63b Separate system analytics 2018-02-22 08:12:15 -08:00
JonnyWong16
3f3d1962c7 Hash client ID 2018-02-22 07:29:56 -08:00
JonnyWong16
655a359ef4 Clean up tracker 2018-02-22 07:28:13 -08:00
JonnyWong16
90647628c9 Test sending install metrics on startup 2018-02-21 10:28:03 -08:00
JonnyWong16
681c3ed6e3 Add Google Universal Analytics 2018-02-21 10:27:28 -08:00
JonnyWong16
7f255943c6 Commit to link to the commit 2018-02-20 15:48:50 -08:00
JonnyWong16
b6e73b5dea Fix fallback thumb for home stats cards 2018-02-20 13:39:41 -08:00
JonnyWong16
eacb7f6ae5 Proper image name for poster uploads 2018-02-19 19:38:36 -08:00
JonnyWong16
7b300bb87e Add "Note" tag for Imgur and 3rd party API message 2018-02-19 19:19:27 -08:00
JonnyWong16
a81ad27d85 Add include subject line for Pushover 2018-02-19 19:10:08 -08:00
JonnyWong16
8eed14ff3b Add posters to Pushbullet notifications 2018-02-19 19:08:52 -08:00
JonnyWong16
82446acdf0 Telegram upload image in single message 2018-02-19 19:05:44 -08:00
JonnyWong16
88770b8805 Imgur upload not required for Pushover posters 2018-02-19 12:36:12 -08:00
JonnyWong16
f9f05bbea3 Add posters to Pushover notifications 2018-02-19 12:32:35 -08:00
JonnyWong16
17dd767c22 Send HipChat header 2018-02-19 11:57:21 -08:00
JonnyWong16
25b1dc6dd8 Fix refresh login logs on user page 2018-02-19 11:42:14 -08:00
JonnyWong16
b2b1277e37 Don't reload table again when switching tabs on user and library pages 2018-02-19 10:59:40 -08:00
JonnyWong16
8e1a588ced Fix conflicting history and sync delete mode on user page 2018-02-19 10:59:10 -08:00
JonnyWong16
9eddfafeae Correct poster height on the watch statistic cards 2018-02-19 10:05:19 -08:00
JonnyWong16
d24a922ccb Adjust media screen size for button bar 2018-02-19 10:00:17 -08:00
JonnyWong16
bbc6482c99 Add edit mode to sync table on user page 2018-02-19 09:11:35 -08:00
JonnyWong16
36ff1fb674 Fix button layout on mobile site 2018-02-19 08:39:27 -08:00
JonnyWong16
f0aa793262 Update wording for group history setting 2018-02-18 13:01:09 -08:00
JonnyWong16
681627a656 Fix user filtering on graphs with grouping 2018-02-18 12:58:40 -08:00
JonnyWong16
87c6ad66fb Add grouping to the remaining graphs 2018-02-18 12:50:28 -08:00
JonnyWong16
4ab9eb3bfa Fix popovers in history table modal 2018-02-18 12:15:11 -08:00
JonnyWong16
2d56ac027b Add plays graph grouping to API docs 2018-02-18 11:06:17 -08:00
JonnyWong16
836c4293d6 Respect group history setting in graphs 2018-02-18 11:04:31 -08:00
JonnyWong16
07092e8aa5 Don't reconnect server when saving settings if server settings are not changed 2018-02-18 11:03:57 -08:00
JonnyWong16
66743c1401 Add conditions bypass message for manual recently added notification trigger 2018-02-18 08:41:55 -08:00
JonnyWong16
bfe34e060b Fix KeyError from 868aeb3 2018-02-18 08:36:53 -08:00
JonnyWong16
5ed4236a22 "Commit" if only one commit behind 2018-02-18 08:33:14 -08:00
JonnyWong16
868aeb3902 Fix notification update parameter types 2018-02-18 08:29:40 -08:00
JonnyWong16
cbcdac5b04 Update message to show release instead of commits for master and beta 2018-02-18 08:28:44 -08:00
JonnyWong16
d473bb3058 Prevent dismissing the modal on the shutdown page 2018-02-17 15:05:54 -08:00
36 changed files with 1371 additions and 323 deletions

11
API.md
View File

@@ -1316,6 +1316,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1341,6 +1342,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1366,6 +1368,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1391,6 +1394,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1416,6 +1420,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1441,6 +1446,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1466,6 +1472,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1491,6 +1498,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1516,6 +1524,7 @@ Optional parameters:
time_range (str): The number of months of data to return time_range (str): The number of months of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1777,6 +1786,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1802,6 +1812,7 @@ Optional parameters:
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:

View File

@@ -1,5 +1,26 @@
# Changelog # Changelog
## v2.0.20-beta (2018-02-24)
* Notifications:
* New: Add poster support for Pushover notifications.
* New: Add poster support for Pushbullet notifications.
* Fix: Incorrect Plex/Tautulli update notification parameter types.
* Change: Poster and text sent as a single message for Telegram.
* Change: Posters uploaded directly to Telegram without Imgur.
* UI:
* New: Add "Delete" button to synced items table on user pages.
* Fix: Button spacing/positioning on mobile site.
* Fix: Music statistic cards not using the fallback thumbnail.
* Fix: Logo not showing up when using an SVG.
* Change: Graphs now respect the "Group History" setting.
* API:
* New: Add grouping to graph API commands.
* Other:
* New: Added Google Analytics to collect installation metrics.
* Fix: Reconnecting to the Plex server when server settings are not changed.
## v2.0.19-beta (2018-02-16) ## v2.0.19-beta (2018-02-16)
* Monitoring: * Monitoring:

View File

@@ -62,7 +62,7 @@ def main():
try: try:
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
plexpy.SYS_ENCODING = locale.getpreferredencoding() plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
except (locale.Error, IOError): except (locale.Error, IOError):
pass pass

View File

@@ -47,11 +47,17 @@
You are running an unknown version of Tautulli.<br /> You are running an unknown version of Tautulli.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> <a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div> </div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win': % elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and plexpy.common.VERSION_NUMBER != plexpy.LATEST_RELEASE:
<div id="updatebar" style="display: none;">
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.INSTALL_TYPE != 'win':
<div id="updatebar" style="display: none;"> <div id="updatebar" style="display: none;">
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank"> A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
newer version</a> is available!<br /> newer version</a> of Tautulli is available!<br />
You are ${plexpy.COMMITS_BEHIND} commits behind.<br /> You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> <a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div> </div>
% else: % else:
@@ -68,7 +74,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="home" title="Tautulli"> <a class="navbar-brand" href="home" title="Tautulli">
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object> <img src="${http_root}images/logo-tautulli-45.png" height="45" alt="PlexPy">
</a> </a>
</div> </div>
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1"> <div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
@@ -311,17 +317,21 @@ ${next.modalIncludes()}
complete: function (xhr, status) { complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = ''; var msg = '';
if (result.update === true) { if (result.update === null) {
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> is available!<br />' + msg = 'You are running an unknown version of Tautulli.<br />' +
'You are '+ result.commits_behind + ' commits behind.<br />' + '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn();
} else if (result.update === true && result.release === true) {
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />' +
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn();
} else if (result.update === true && result.release === false) {
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />' +
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>'; '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn(); $('#updatebar').html(msg).fadeIn();
} else if (result.update === false) { } else if (result.update === false) {
showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000); showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
} else if (result.update === null) {
msg = 'You are running an unknown version of Tautulli.<br />' +
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn();
} }
if (_callback) { if (_callback) {

View File

@@ -26,7 +26,7 @@ DOCUMENTATION :: END
</tr> </tr>
<tr> <tr>
<td>Git Commit Hash:</td> <td>Git Commit Hash:</td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CURRENT_VERSION}</a></td> <td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}">${plexpy.CURRENT_VERSION}</a></td>
</tr> </tr>
% endif % endif
<tr> <tr>

View File

@@ -359,11 +359,13 @@ table.display tr.shown + tr:hover {
} }
table.display tr.shown + tr:hover a, table.display tr.shown + tr:hover a,
table.display tr.shown + tr td:hover a, table.display tr.shown + tr td:hover a,
table.display tr.shown + tr td:hover a .fa,
table.display tr.shown + tr .pagination > .active > a, table.display tr.shown + tr .pagination > .active > a,
table.display tr.shown + tr .pagination > .active > a:hover { table.display tr.shown + tr .pagination > .active > a:hover {
color: #fff; color: #fff;
} }
table.display tr.shown + tr table[id^='history_child'] td:hover a, table.display tr.shown + tr table[id^='history_child'] td:hover a,
table.display tr.shown + tr table[id^='history_child'] td:hover a .fa,
table.display tr.shown + tr table[id^='media_info_child'] > tr > td:hover a, table.display tr.shown + tr table[id^='media_info_child'] > tr > td:hover a,
table.display tr.shown + tr table[id^='media_info_child'] tr.shown + tr table[id^='media_info_child'] td:hover a { table.display tr.shown + tr table[id^='media_info_child'] tr.shown + tr table[id^='media_info_child'] td:hover a {
color: #cc7b19; color: #cc7b19;

View File

@@ -2395,6 +2395,9 @@ a .library-user-instance-box:hover {
margin-top: 9px; margin-top: 9px;
width: 175px; width: 175px;
} }
.home-padded-header .button-bar {
float: left;
}
.home-platforms { .home-platforms {
} }
.home-platforms ul, .home-platforms ul,
@@ -3146,7 +3149,7 @@ div.dataTables_info {
border-radius: 2px; border-radius: 2px;
} }
.history-thumbnail-popover { .history-thumbnail-popover {
z-index: 2; z-index: 2000;
padding: 0; padding: 0;
border: 0; border: 0;
} }
@@ -3316,6 +3319,48 @@ pre::-webkit-scrollbar-thumb {
width: 100%; width: 100%;
} }
} }
@media only screen
and (min-device-width: 300px)
and (max-device-width: 740px) {
.header-bar {
display: block;
float: none !important;
}
.button-bar {
float: left !important;
clear: both;
margin-top: 15px;
}
.button-bar > div,
.button-bar > button,
.button-bar > span {
float: left !important;
clear: both !important;
margin-bottom: 10px;
}
.button-bar > div > button.btn {
float: left !important;
clear: both !important;
}
.home-padded-header .button-bar {
margin-top: 10px;
margin-bottom: 15px;
}
}
@media only screen
and (min-device-width: 740px)
and (max-device-width: 1024px) {
.button-bar {
float: right !important;
}
.button-bar > div > button.btn {
float: left !important;
clear: both !important;
}
.home-padded-header .button-bar {
float: left !important;
}
}
#search_form { #search_form {
width: 300px; width: 300px;
padding: 8px 15px; padding: 8px 15px;

View File

@@ -11,7 +11,7 @@
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-bar-chart"></i> Graphs</span> <span><i class="fa fa-bar-chart"></i> Graphs</span>
</div> </div>
<div class="button-bar hidden-xs"> <div class="button-bar">
<div class="btn-group" id="user-selection"> <div class="btn-group" id="user-selection">
<label> <label>
<select name="graph-user" id="graph-user" class="btn" style="color: inherit;"> <select name="graph-user" id="graph-user" class="btn" style="color: inherit;">

View File

@@ -5,7 +5,15 @@
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title" id="myModalLabel"> <h4 class="modal-title" id="myModalLabel">
<strong><span id="modal_header_ip_address"> <strong><span id="modal_header_ip_address">
% if data.get('media_type'):
<% h = {'episode': 'TV Show', 'track': 'Music'} %>
<i class="fa fa-history"></i> ${h.get(data['media_type'], data['media_type'].title())} History for <span id="date-header">${data['start_date']}</span>
% elif data.get('transcode_decision'):
<% h = {'copy': 'Direct Stream'} %>
<i class="fa fa-history"></i> ${h.get(data['transcode_decision'], data['transcode_decision'].title())} History for <span id="date-header">${data['start_date']}</span>
% else:
<i class="fa fa-history"></i> History for <span id="date-header">${data['start_date']}</span> <i class="fa fa-history"></i> History for <span id="date-header">${data['start_date']}</span>
% endif
</span></strong> </span></strong>
</h4> </h4>
</div> </div>
@@ -13,11 +21,18 @@
<table class="display history_table" id="history_table_modal" width="100%"> <table class="display history_table" id="history_table_modal" width="100%">
<thead> <thead>
<tr> <tr>
<th align="left" id="started">Started</th> <th align="left" id="delete_row">Delete</th>
<th align="left" id="stopped">Stopped</th> <th align="left" id="date">Date</th>
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="player">Player</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th>
<th align="left" id="device">Player</th>
<th align="left" id="title">Title</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> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -28,28 +43,31 @@
</div> </div>
</div> </div>
<script src="${http_root}js/tables/history_table_modal.js${cache_param}"></script> <script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$('#date-header').html(moment('${data["start_date"]}','YYYY-MM-DD').format('ddd MMM Do YYYY')); $('#date-header').html(moment('${data["start_date"]}','YYYY-MM-DD').format('ddd MMM Do YYYY'));
history_table_modal_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post',
data: function ( d ) { data: function ( d ) {
return { return {
json_data: JSON.stringify(d), json_data: JSON.stringify(d),
grouping: false,
user_id: "${data['user_id']}", user_id: "${data['user_id']}",
start_date: "${data['start_date']}", start_date: "${data['start_date']}",
media_type: "${data.get('media_type')}", media_type: "${data.get('media_type')}",
transcode_decision: "${data.get('transcode_decision')}" transcode_decision: "${data.get('transcode_decision')}"
}; };
} }
} };
history_table = $('#history_table_modal').DataTable(history_table_options);
history_table.columns([0, 3, 4, 8, 10, 11]).visible(false);
history_table = $('#history_table_modal').DataTable(history_table_modal_options);
clearSearchButton('history_table_modal', history_table); clearSearchButton('history_table_modal', history_table);
$('#history-modal').on('shown.bs.modal', function() {
history_table.columns.adjust().draw();
});
}); });
</script> </script>
% else: % else:

View File

@@ -88,11 +88,11 @@ DOCUMENTATION :: END
% if stat_id in ('top_music', 'popular_music'): % if stat_id in ('top_music', 'popular_music'):
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster-blur" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=cover);"></div> <div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster-blur" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=cover);"></div>
% endif % endif
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else 'poster' %> <% height, type = ('300', 'cover') if stat_id in ('top_music', 'popular_music') else ('450', 'poster') %>
<% href = 'info?rating_key={}'.format(row0['rating_key']) if row0['rating_key'] else '#' %> <% href = 'info?rating_key={}'.format(row0['rating_key']) if row0['rating_key'] else '#' %>
<a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}"> <a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}">
% if row0['thumb']: % if row0['thumb']:
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=${type});"></div> <div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=${height}&fallback=${type});"></div>
% else: % else:
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(images/${type}.png);"></div> <div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(images/${type}.png);"></div>
% endif % endif
@@ -217,7 +217,8 @@ DOCUMENTATION :: END
$('#stats-thumb-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')'); $('#stats-thumb-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')'); $('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
} else { } else {
$('#stats-background-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)'); $('#stats-thumb-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(images/' + fallback + '.png)');
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -31,27 +31,29 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="home-padded-header padded-header"> <div class="home-padded-header padded-header">
<h3 class="pull-left">Watch Statistics</h3> <h3 class="pull-left">Watch Statistics</h3>
<div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px"> <div class="button-bar">
% if config['home_stats_type'] == 0: <div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px">
<label class="btn btn-dark active"> % if config['home_stats_type'] == 0:
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off" checked> Play Count <label class="btn btn-dark active">
</label> <input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off" checked> Play Count
<label class="btn btn-dark"> </label>
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off"> Play Duration <label class="btn btn-dark">
</label> <input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off"> Play Duration
% else: </label>
<label class="btn btn-dark"> % else:
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off"> Play Count <label class="btn btn-dark">
</label> <input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off"> Play Count
<label class="btn btn-dark active"> </label>
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off" checked> Play Duration <label class="btn btn-dark active">
</label> <input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off" checked> Play Duration
% endif </label>
</div> % endif
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection"> </div>
<span class="input-group-addon btn-dark inactive">Last</span> <div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection">
<input type="number" class="form-control" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" /> <span class="input-group-addon btn-dark inactive">Last</span>
<span class="input-group-addon btn-dark inactive">days</span> <input type="number" class="form-control" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
<span class="input-group-addon btn-dark inactive">days</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -69,7 +71,9 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="home-padded-header padded-header" id="library-statistics-header"> <div class="home-padded-header padded-header" id="library-statistics-header">
<h3 class="pull-left">Library Statistics</h3> <h3 class="pull-left">Library Statistics</h3>
<span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span> <div class="button-bar">
<span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -94,23 +98,25 @@
<a href="#" id="recently-added-page-right" class="paginate btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a> <a href="#" id="recently-added-page-right" class="paginate btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
</li> </li>
</ul> </ul>
<div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px"> <div class="button-bar">
<label class="btn btn-dark active" id="recently-added-label-all"> <div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="" autocomplete="off"> All <label class="btn btn-dark active" id="recently-added-label-all">
</label> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="" autocomplete="off"> All
<label class="btn btn-dark" id="recently-added-label-movies"> </label>
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies <label class="btn btn-dark" id="recently-added-label-movies">
</label> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies
<label class="btn btn-dark" id="recently-added-label-tv"> </label>
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows <label class="btn btn-dark" id="recently-added-label-tv">
</label> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows
<label class="btn btn-dark" id="recently-added-label-music"> </label>
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-music" value="artist" autocomplete="off"> Music <label class="btn btn-dark" id="recently-added-label-music">
</label> <input type="radio" name="recently-added-toggle" id="recently-added-toggle-music" value="artist" autocomplete="off"> Music
</div> </label>
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection"> </div>
<input type="number" class="form-control" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" /> <div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection">
<span class="input-group-addon btn-dark inactive">items</span> <input type="number" class="form-control" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" />
<span class="input-group-addon btn-dark inactive">items</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -521,6 +521,7 @@ DOCUMENTATION :: END
% endfor % endfor
</select> </select>
</div> </div>
<p class="help-block">Note: All custom notification conditions will be bypassed.</p>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@@ -270,7 +270,7 @@ history_table_options = {
}); });
if ($('#row-edit-mode').hasClass('active')) { if ($('#row-edit-mode').hasClass('active')) {
$('.delete-control').each(function () { $('.history_table .delete-control').each(function () {
$(this).removeClass('hidden'); $(this).removeClass('hidden');
}); });
} }
@@ -290,7 +290,9 @@ history_table_options = {
}, },
"preDrawCallback": function(settings) { "preDrawCallback": function(settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows..."; var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0) showMsg(msg, false, false, 0);
$('[data-toggle="tooltip"]').tooltip('destroy');
$('[data-toggle="popover"]').popover('destroy');
}, },
"rowCallback": function (row, rowData, rowIndex) { "rowCallback": function (row, rowData, rowIndex) {
if (rowData['group_count'] == 1) { if (rowData['group_count'] == 1) {
@@ -464,7 +466,7 @@ function childTableOptions(rowData) {
}); });
if ($('#row-edit-mode').hasClass('active')) { if ($('#row-edit-mode').hasClass('active')) {
$('.delete-control').each(function () { $('.history_table .delete-control').each(function () {
$(this).removeClass('hidden'); $(this).removeClass('hidden');
}); });
} }

View File

@@ -113,7 +113,7 @@ login_log_table_options = {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows..."; var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0) showMsg(msg, false, false, 0)
} }
} };
$('.login_log_table').on('click', '> tbody > tr > td.modal-control-ip', function () { $('.login_log_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
var tr = $(this).closest('tr'); var tr = $(this).closest('tr');

View File

@@ -139,6 +139,13 @@ sync_table_options = {
// $('html,body').scrollTop(0); // $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut(); $('#ajaxMsg').fadeOut();
if ($('#sync-row-edit-mode').hasClass('active')) {
$('.sync_table .delete-control').each(function () {
$(this).removeClass('hidden');
});
}
}, },
"preDrawCallback": function (settings) { "preDrawCallback": function (settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows..."; var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
@@ -146,7 +153,7 @@ sync_table_options = {
} }
}; };
$('#sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () { $('.sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
var tr = $(this).parents('tr'); var tr = $(this).parents('tr');
var row = sync_table.row(tr); var row = sync_table.row(tr);
var rowData = row.data(); var rowData = row.data();

View File

@@ -382,7 +382,7 @@ DOCUMENTATION :: END
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
}; };
} }
} };
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options); history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
@@ -392,7 +392,13 @@ DOCUMENTATION :: END
} }
$('a[href="#tabs-history"]').on('shown.bs.tab', function() { $('a[href="#tabs-history"]').on('shown.bs.tab', function() {
loadHistoryTable(); if (typeof(history_table) === 'undefined') {
loadHistoryTable();
}
});
$("#refresh-history-list").click(function () {
history_table.draw();
}); });
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -408,7 +414,7 @@ DOCUMENTATION :: END
refresh: refresh_table refresh: refresh_table
}; };
} }
} };
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options); media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' }); var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
@@ -418,7 +424,9 @@ DOCUMENTATION :: END
} }
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() { $('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
loadMediaInfoTable(); if (typeof(media_info_table) === 'undefined') {
loadMediaInfoTable();
}
}); });
$("#refresh-media-info-table").click(function () { $("#refresh-media-info-table").click(function () {
@@ -484,10 +492,6 @@ DOCUMENTATION :: END
}); });
% endif % endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() { function recentlyWatched() {
// Populate recently watched // Populate recently watched
$.ajax({ $.ajax({

View File

@@ -37,7 +37,7 @@
<div class="row"> <div class="row">
<div class="login-container"> <div class="login-container">
<div class="login-logo"> <div class="login-logo">
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 100px;"></object> <img src="${http_root}images/logo-tautulli-100.png" height="100" alt="PlexPy">
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-sm-offset-3"> <div class="col-sm-6 col-sm-offset-3">

View File

@@ -113,9 +113,9 @@
</div> </div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
<label> <label>
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Table and Watch Statistics History <input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Successive Play History
</label> </label>
<p class="help-block">Group successive play history by the same user as a single entry in the tables and watch statistics.</p> <p class="help-block">Group successive play history by the same user as a single entry in the watch statistics, tables, and graphs.</p>
</div> </div>
<div class="checkbox advanced-setting"> <div class="checkbox advanced-setting">
<label> <label>

View File

@@ -41,13 +41,16 @@
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<script> <script>
// Remove the update bar
$('#updatebar').remove();
// Use p.countdown as container, pass redirect, duration, and optional message // Use p.countdown as container, pass redirect, duration, and optional message
$(".countdown").countdown(reloadPage, ${timer}, ""); $(".countdown").countdown(reloadPage, ${timer}, "");
$('#state-change-modal').modal({
keyboard: false
})
// Make modal visible // Make modal visible
$('#state-change-modal').modal('show') $('#state-change-modal').modal({
backdrop: 'static',
keyboard: false
}).show();
// Redirect to home page after countdown. // Redirect to home page after countdown.
function reloadPage() { function reloadPage() {

View File

@@ -20,10 +20,10 @@
</div> </div>
<div class="button-bar"> <div class="button-bar">
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect syncs to delete. Data is deleted upon exiting edit mode.</div> <div class="alert alert-danger alert-edit" role="alert" id="sync-row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect syncs to delete. Data is deleted upon exiting delete mode.</div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"> <button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="sync-row-edit-mode">
<i class="fa fa-pencil"></i> Edit mode <i class="fa fa-trash-o"></i> Delete mode
</button>&nbsp </button>&nbsp
</div> </div>
% endif % endif
@@ -44,7 +44,7 @@
</div> </div>
</div> </div>
<div class='table-card-back'> <div class='table-card-back'>
<table class="display" id="sync_table" width="100%"> <table class="display sync_table" id="sync_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align="left" id="delete_row">Delete</th> <th align="left" id="delete_row">Delete</th>
@@ -138,8 +138,8 @@
loadSyncTable(selected_user_id); loadSyncTable(selected_user_id);
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
$('#row-edit-mode').on('click', function() { $('#sync-row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200); $('#sync-row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) { if ($(this).hasClass('active')) {
if (syncs_to_delete.length > 0) { if (syncs_to_delete.length > 0) {
@@ -161,13 +161,13 @@
} }
}); });
}); });
sync_table.draw(); sync_table.ajax.reload();
}); });
} }
$('.delete-control').each(function () { $('.delete-control').each(function () {
$(this).addClass('hidden'); $(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200); $('#sync-row-edit-mode-alert').fadeOut(200);
}); });
} else { } else {
@@ -182,7 +182,7 @@
}); });
$("#refresh-syncs-list").click(function() { $("#refresh-syncs-list").click(function() {
sync_table.draw(); sync_table.ajax.reload();
}); });
</script> </script>
</%def> </%def>

View File

@@ -213,13 +213,25 @@ DOCUMENTATION :: END
</span> </span>
</div> </div>
<div class="button-bar"> <div class="button-bar">
% if _session['user_group'] == 'admin':
<div class="alert alert-danger alert-edit" role="alert" id="sync-row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect syncs to delete. Data is deleted upon exiting delete mode.</div>
<div class="btn-group">
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="sync-row-edit-mode">
<i class="fa fa-trash-o"></i> Delete mode
</button>&nbsp
</div>
% endif
<div class="btn-group">
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-sync"></div> <div class="btn-group colvis-button-bar" id="button-bar-sync"></div>
</div> </div>
</div> </div>
<div class="table-card-back"> <div class="table-card-back">
<table class="display" id="sync_table-UID-${data['user_id']}" width="100%"> <table class="display sync_table" id="sync_table-UID-${data['user_id']}" width="100%">
<thead> <thead>
<tr> <tr>
<th align="left" id="delete_row">Delete</th>
<th align="left" id="state">State</th> <th align="left" id="state">State</th>
<th align="left" id="username">Username</th> <th align="left" id="username">Username</th>
<th align="left" id="sync_title">Title</th> <th align="left" id="sync_title">Title</th>
@@ -252,6 +264,11 @@ DOCUMENTATION :: END
</strong> </strong>
</span> </span>
</div> </div>
<div class="button-bar">
<div class="btn-group">
<button class="btn btn-dark refresh-ip-address-button" id="refresh-ip-address-list"><i class="fa fa-refresh"></i> Refresh IP addresses</button>
</div>
</div>
</div> </div>
<div class="table-card-back"> <div class="table-card-back">
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%"> <table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
@@ -284,6 +301,9 @@ DOCUMENTATION :: END
</span> </span>
</div> </div>
<div class="button-bar"> <div class="button-bar">
<div class="btn-group">
<button class="btn btn-dark refresh-login-button" id="refresh-login-list"><i class="fa fa-refresh"></i> Refresh logins</button>
</div>
<div class="btn-group colvis-button-bar" id="button-bar-login"></div> <div class="btn-group colvis-button-bar" id="button-bar-login"></div>
</div> </div>
</div> </div>
@@ -298,6 +318,7 @@ DOCUMENTATION :: END
<th align="left" id="host">Host</th> <th align="left" id="host">Host</th>
<th align="left" id="os">Operating System</th> <th align="left" id="os">Operating System</th>
<th align="left" id="browser">Browser</th> <th align="left" id="browser">Browser</th>
<th align="left" id="login_success"></th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@@ -351,7 +372,7 @@ DOCUMENTATION :: END
<h4 class="modal-title" id="myModalLabel">Confirm Delete</h4> <h4 class="modal-title" id="myModalLabel">Confirm Delete</h4>
</div> </div>
<div class="modal-body" style="text-align: center;"> <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>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> <span id="deleteType"></span> item(s)?</p>
<p>This is permanent and cannot be undone!</p> <p>This is permanent and cannot be undone!</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -388,11 +409,6 @@ DOCUMENTATION :: END
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust(); $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
}); });
$('a[href="#tabs-profile"]').on('shown.bs.tab', function() {
var media_type = null;
loadHistoryTable(media_type);
});
function loadHistoryTable(media_type) { function loadHistoryTable(media_type) {
// Build watch history table // Build watch history table
history_table_options.ajax = { history_table_options.ajax = {
@@ -405,7 +421,7 @@ DOCUMENTATION :: END
media_type: media_type media_type: media_type
}; };
} }
} };
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options); history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
history_table.column(2).visible(false); history_table.column(2).visible(false);
@@ -423,29 +439,21 @@ DOCUMENTATION :: END
}); });
} }
$('a[href="#tabs-history"]').on('shown.bs.tab', function() { function loadSyncTable() {
var media_type = null;
loadHistoryTable(media_type);
});
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
// Build user sync table // Build user sync table
sync_table_options.ajax = { sync_table_options.ajax = {
url: 'get_sync', url: 'get_sync?user_id=' + user_id
data: function(d) { };
d.user_id = user_id;
}
}
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options); sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
sync_table.column(1).visible(false); sync_table.column(2).visible(false);
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } ); var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0] } );
$( colvis_sync.button() ).appendTo('#button-bar-sync'); $( colvis_sync.button() ).appendTo('#button-bar-sync');
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table); clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
}); }
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() { function loadIPAddressTable() {
// Build user IP table // Build user IP table
user_ip_table_options.ajax = { user_ip_table_options.ajax = {
url: 'get_user_ips', url: 'get_user_ips',
@@ -456,27 +464,71 @@ DOCUMENTATION :: END
user_id: user_id user_id: user_id
}; };
} }
} };
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options); user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table); clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
}); }
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() { function loadLoginTable() {
// Build user login table // Build user login table
login_log_table_options.ajax = { login_log_table_options.ajax = {
url: 'get_user_logins', url: 'get_user_logins',
data: function(d) { data: function(d) {
d.user_id = user_id; return {
json_data: JSON.stringify(d),
user_id: user_id
};
} }
} };
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options); login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
login_log_table.columns([1, 2]).visible(false); login_log_table.columns([1, 2]).visible(false);
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } ); var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [7] } );
$( colvis_login.button() ).appendTo('#button-bar-login'); $( colvis_login.button() ).appendTo('#button-bar-login');
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table); clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
}
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
if (typeof(history_table) === 'undefined') {
var media_type = null;
loadHistoryTable(media_type);
}
});
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
if (typeof(sync_table) === 'undefined') {
loadSyncTable(user_id);
}
});
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
if (typeof(user_ip_table) === 'undefined') {
loadIPAddressTable(user_id);
}
});
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
if (typeof(login_log_table) === 'undefined') {
loadLoginTable(user_id);
}
});
$("#refresh-history-list").click(function () {
history_table.draw();
});
$("#refresh-syncs-list").click(function() {
sync_table.ajax.reload();
});
$("#refresh-ip-address-list").click(function () {
user_ip_table.draw();
});
$("#refresh-login-list").click(function () {
login_log_table.draw();
}); });
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -502,6 +554,7 @@ DOCUMENTATION :: END
if ($(this).hasClass('active')) { if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) { if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length); $('#deleteCount').text(history_to_delete.length);
$('#deleteType').text('history');
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) { history_to_delete.forEach(function(row, idx) {
@@ -520,14 +573,56 @@ DOCUMENTATION :: END
}); });
} }
$('.delete-control').each(function () { $('.history_table .delete-control').each(function () {
$(this).addClass('hidden'); $(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200); $('#row-edit-mode-alert').fadeOut(200);
}); });
} else { } else {
history_to_delete = []; history_to_delete = [];
$('.delete-control').each(function() { $('.history_table .delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
$('#sync-row-edit-mode').on('click', function() {
$('#sync-row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (syncs_to_delete.length > 0) {
$('#deleteCount').text(syncs_to_delete.length);
$('#deleteType').text('sync');
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
syncs_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_sync_rows',
type: 'POST',
data: {
client_id: row.client_id,
sync_id: row.sync_id
},
async: true,
success: function (data) {
var msg = "Sync deleted";
showMsg(msg, false, true, 2000);
}
});
});
sync_table.ajax.reload();
});
}
$('.sync_table .delete-control').each(function () {
$(this).addClass('hidden');
$('#sync-row-edit-mode-alert').fadeOut(200);
});
} else {
syncs_to_delete = [];
$('.sync_table .delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden'); $(this).removeClass('hidden');
}); });
@@ -535,10 +630,6 @@ DOCUMENTATION :: END
}); });
% endif % endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() { function recentlyWatched() {
// Populate recently watched // Populate recently watched
$.ajax({ $.ajax({

View File

@@ -51,7 +51,7 @@
<form> <form>
<div class="wizard-card" data-cardname="card1"> <div class="wizard-card" data-cardname="card1">
<div style="float: right;"> <div style="float: right;">
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object> <img src="${http_root}images/logo-tautulli-45.png" height="45" alt="PlexPy">
</div> </div>
<h3 style="line-height: 50px;">Welcome!</h3> <h3 style="line-height: 50px;">Welcome!</h3>
<br /> <br />

View File

@@ -0,0 +1,121 @@
#!/usr/bin/python
###############################################################################
# Formatting filter for urllib2's HTTPHandler(debuglevel=1) output
# Copyright (c) 2013, Analytics Pros
#
# This project is free software, distributed under the BSD license.
# Analytics Pros offers consulting and integration services if your firm needs
# assistance in strategy, implementation, or auditing existing work.
###############################################################################
import sys, re, os
from cStringIO import StringIO
class BufferTranslator(object):
""" Provides a buffer-compatible interface for filtering buffer content.
"""
parsers = []
def __init__(self, output):
self.output = output
self.encoding = getattr(output, 'encoding', None)
def write(self, content):
content = self.translate(content)
self.output.write(content)
@staticmethod
def stripslashes(content):
return content.decode('string_escape')
@staticmethod
def addslashes(content):
return content.encode('string_escape')
def translate(self, line):
for pattern, method in self.parsers:
match = pattern.match(line)
if match:
return method(match)
return line
class LineBufferTranslator(BufferTranslator):
""" Line buffer implementation supports translation of line-format input
even when input is not already line-buffered. Caches input until newlines
occur, and then dispatches translated input to output buffer.
"""
def __init__(self, *a, **kw):
self._linepending = []
super(LineBufferTranslator, self).__init__(*a, **kw)
def write(self, _input):
lines = _input.splitlines(True)
for i in range(0, len(lines)):
last = i
if lines[i].endswith('\n'):
prefix = len(self._linepending) and ''.join(self._linepending) or ''
self.output.write(self.translate(prefix + lines[i]))
del self._linepending[0:]
last = -1
if last >= 0:
self._linepending.append(lines[ last ])
def __del__(self):
if len(self._linepending):
self.output.write(self.translate(''.join(self._linepending)))
class HTTPTranslator(LineBufferTranslator):
""" Translates output from |urllib2| HTTPHandler(debuglevel = 1) into
HTTP-compatible, readible text structures for human analysis.
"""
RE_LINE_PARSER = re.compile(r'^(?:([a-z]+):)\s*(\'?)([^\r\n]*)\2(?:[\r\n]*)$')
RE_LINE_BREAK = re.compile(r'(\r?\n|(?:\\r)?\\n)')
RE_HTTP_METHOD = re.compile(r'^(POST|GET|HEAD|DELETE|PUT|TRACE|OPTIONS)')
RE_PARAMETER_SPACER = re.compile(r'&([a-z0-9]+)=')
@classmethod
def spacer(cls, line):
return cls.RE_PARAMETER_SPACER.sub(r' &\1= ', line)
def translate(self, line):
parsed = self.RE_LINE_PARSER.match(line)
if parsed:
value = parsed.group(3)
stage = parsed.group(1)
if stage == 'send': # query string is rendered here
return '\n# HTTP Request:\n' + self.stripslashes(value)
elif stage == 'reply':
return '\n\n# HTTP Response:\n' + self.stripslashes(value)
elif stage == 'header':
return value + '\n'
else:
return value
return line
def consume(outbuffer = None): # Capture standard output
sys.stdout = HTTPTranslator(outbuffer or sys.stdout)
return sys.stdout
if __name__ == '__main__':
consume(sys.stdout).write(sys.stdin.read())
print '\n'
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4

View File

@@ -0,0 +1,433 @@
###############################################################################
# Universal Analytics for Python
# Copyright (c) 2013, Analytics Pros
#
# This project is free software, distributed under the BSD license.
# Analytics Pros offers consulting and integration services if your firm needs
# assistance in strategy, implementation, or auditing existing work.
###############################################################################
from urllib2 import urlopen, build_opener, install_opener
from urllib2 import Request, HTTPSHandler
from urllib2 import URLError, HTTPError
from urllib import urlencode
import random
import datetime
import time
import uuid
import hashlib
import socket
def generate_uuid(basedata=None):
""" Provides a _random_ UUID with no input, or a UUID4-format MD5 checksum of any input data provided """
if basedata is None:
return str(uuid.uuid4())
elif isinstance(basedata, basestring):
checksum = hashlib.md5(basedata).hexdigest()
return '%8s-%4s-%4s-%4s-%12s' % (
checksum[0:8], checksum[8:12], checksum[12:16], checksum[16:20], checksum[20:32])
class Time(datetime.datetime):
""" Wrappers and convenience methods for processing various time representations """
@classmethod
def from_unix(cls, seconds, milliseconds=0):
""" Produce a full |datetime.datetime| object from a Unix timestamp """
base = list(time.gmtime(seconds))[0:6]
base.append(milliseconds * 1000) # microseconds
return cls(*base)
@classmethod
def to_unix(cls, timestamp):
""" Wrapper over time module to produce Unix epoch time as a float """
if not isinstance(timestamp, datetime.datetime):
raise TypeError, 'Time.milliseconds expects a datetime object'
base = time.mktime(timestamp.timetuple())
return base
@classmethod
def milliseconds_offset(cls, timestamp, now=None):
""" Offset time (in milliseconds) from a |datetime.datetime| object to now """
if isinstance(timestamp, (int, float)):
base = timestamp
else:
base = cls.to_unix(timestamp)
base = base + (timestamp.microsecond / 1000000)
if now is None:
now = time.time()
return (now - base) * 1000
class HTTPRequest(object):
""" URL Construction and request handling abstraction.
This is not intended to be used outside this module.
Automates mapping of persistent state (i.e. query parameters)
onto transcient datasets for each query.
"""
endpoint = 'https://www.google-analytics.com/collect'
@staticmethod
def debug():
""" Activate debugging on urllib2 """
handler = HTTPSHandler(debuglevel=1)
opener = build_opener(handler)
install_opener(opener)
# Store properties for all requests
def __init__(self, user_agent=None, *args, **opts):
self.user_agent = user_agent or 'Analytics Pros - Universal Analytics (Python)'
@classmethod
def fixUTF8(cls, data): # Ensure proper encoding for UA's servers...
""" Convert all strings to UTF-8 """
for key in data:
if isinstance(data[key], basestring):
data[key] = data[key].encode('utf-8')
return data
# Apply stored properties to the given dataset & POST to the configured endpoint
def send(self, data):
request = Request(
self.endpoint + '?' + urlencode(self.fixUTF8(data)),
headers={
'User-Agent': self.user_agent
}
)
self.open(request)
def open(self, request):
try:
return urlopen(request)
except HTTPError as e:
return False
except URLError as e:
self.cache_request(request)
return False
def cache_request(self, request):
# TODO: implement a proper caching mechanism here for re-transmitting hits
# record = (Time.now(), request.get_full_url(), request.get_data(), request.headers)
pass
class HTTPPost(HTTPRequest):
# Apply stored properties to the given dataset & POST to the configured endpoint
def send(self, data):
request = Request(
self.endpoint,
data=urlencode(self.fixUTF8(data)),
headers={
'User-Agent': self.user_agent
}
)
self.open(request)
class Tracker(object):
""" Primary tracking interface for Universal Analytics """
params = None
parameter_alias = {}
valid_hittypes = ('pageview', 'event', 'social', 'screenview', 'transaction', 'item', 'exception', 'timing')
@classmethod
def alias(cls, typemap, base, *names):
""" Declare an alternate (humane) name for a measurement protocol parameter """
cls.parameter_alias[base] = (typemap, base)
for i in names:
cls.parameter_alias[i] = (typemap, base)
@classmethod
def coerceParameter(cls, name, value=None):
if isinstance(name, basestring) and name[0] == '&':
return name[1:], str(value)
elif name in cls.parameter_alias:
typecast, param_name = cls.parameter_alias.get(name)
return param_name, typecast(value)
else:
raise KeyError, 'Parameter "{0}" is not recognized'.format(name)
def payload(self, data):
for key, value in data.iteritems():
try:
yield self.coerceParameter(key, value)
except KeyError:
continue
option_sequence = {
'pageview': [(basestring, 'dp')],
'event': [(basestring, 'ec'), (basestring, 'ea'), (basestring, 'el'), (int, 'ev')],
'social': [(basestring, 'sn'), (basestring, 'sa'), (basestring, 'st')],
'timing': [(basestring, 'utc'), (basestring, 'utv'), (basestring, 'utt'), (basestring, 'utl')]
}
@classmethod
def consume_options(cls, data, hittype, args):
""" Interpret sequential arguments related to known hittypes based on declared structures """
opt_position = 0
data['t'] = hittype # integrate hit type parameter
if hittype in cls.option_sequence:
for expected_type, optname in cls.option_sequence[hittype]:
if opt_position < len(args) and isinstance(args[opt_position], expected_type):
data[optname] = args[opt_position]
opt_position += 1
@classmethod
def hittime(cls, timestamp=None, age=None, milliseconds=None):
""" Returns an integer represeting the milliseconds offset for a given hit (relative to now) """
if isinstance(timestamp, (int, float)):
return int(Time.milliseconds_offset(Time.from_unix(timestamp, milliseconds=milliseconds)))
if isinstance(timestamp, datetime.datetime):
return int(Time.milliseconds_offset(timestamp))
if isinstance(age, (int, float)):
return int(age * 1000) + (milliseconds or 0)
@property
def account(self):
return self.params.get('tid', None)
def __init__(self, account, name=None, client_id=None, hash_client_id=False, user_id=None, user_agent=None,
use_post=True):
if use_post is False:
self.http = HTTPRequest(user_agent=user_agent)
else:
self.http = HTTPPost(user_agent=user_agent)
self.params = {'v': 1, 'tid': account}
if client_id is None:
client_id = generate_uuid()
self.params['cid'] = client_id
self.hash_client_id = hash_client_id
if user_id is not None:
self.params['uid'] = user_id
def set_timestamp(self, data):
""" Interpret time-related options, apply queue-time parameter as needed """
if 'hittime' in data: # an absolute timestamp
data['qt'] = self.hittime(timestamp=data.pop('hittime', None))
if 'hitage' in data: # a relative age (in seconds)
data['qt'] = self.hittime(age=data.pop('hitage', None))
def send(self, hittype, *args, **data):
""" Transmit HTTP requests to Google Analytics using the measurement protocol """
if hittype not in self.valid_hittypes:
raise KeyError('Unsupported Universal Analytics Hit Type: {0}'.format(repr(hittype)))
self.set_timestamp(data)
self.consume_options(data, hittype, args)
for item in args: # process dictionary-object arguments of transcient data
if isinstance(item, dict):
for key, val in self.payload(item):
data[key] = val
for k, v in self.params.iteritems(): # update only absent parameters
if k not in data:
data[k] = v
data = dict(self.payload(data))
if self.hash_client_id:
data['cid'] = generate_uuid(data['cid'])
# Transmit the hit to Google...
self.http.send(data)
# Setting persistent attibutes of the session/hit/etc (inc. custom dimensions/metrics)
def set(self, name, value=None):
if isinstance(name, dict):
for key, value in name.iteritems():
try:
param, value = self.coerceParameter(key, value)
self.params[param] = value
except KeyError:
pass
elif isinstance(name, basestring):
try:
param, value = self.coerceParameter(name, value)
self.params[param] = value
except KeyError:
pass
def __getitem__(self, name):
param, value = self.coerceParameter(name, None)
return self.params.get(param, None)
def __setitem__(self, name, value):
param, value = self.coerceParameter(name, value)
self.params[param] = value
def __delitem__(self, name):
param, value = self.coerceParameter(name, None)
if param in self.params:
del self.params[param]
def safe_unicode(obj):
""" Safe convertion to the Unicode string version of the object """
try:
return unicode(obj)
except UnicodeDecodeError:
return obj.decode('utf-8')
# Declaring name mappings for Measurement Protocol parameters
MAX_CUSTOM_DEFINITIONS = 200
MAX_EC_LISTS = 11 # 1-based index
MAX_EC_PRODUCTS = 11 # 1-based index
MAX_EC_PROMOTIONS = 11 # 1-based index
Tracker.alias(int, 'v', 'protocol-version')
Tracker.alias(safe_unicode, 'cid', 'client-id', 'clientId', 'clientid')
Tracker.alias(safe_unicode, 'tid', 'trackingId', 'account')
Tracker.alias(safe_unicode, 'uid', 'user-id', 'userId', 'userid')
Tracker.alias(safe_unicode, 'uip', 'user-ip', 'userIp', 'ipaddr')
Tracker.alias(safe_unicode, 'ua', 'userAgent', 'userAgentOverride', 'user-agent')
Tracker.alias(safe_unicode, 'dp', 'page', 'path')
Tracker.alias(safe_unicode, 'dt', 'title', 'pagetitle', 'pageTitle' 'page-title')
Tracker.alias(safe_unicode, 'dl', 'location')
Tracker.alias(safe_unicode, 'dh', 'hostname')
Tracker.alias(safe_unicode, 'sc', 'sessioncontrol', 'session-control', 'sessionControl')
Tracker.alias(safe_unicode, 'dr', 'referrer', 'referer')
Tracker.alias(int, 'qt', 'queueTime', 'queue-time')
Tracker.alias(safe_unicode, 't', 'hitType', 'hittype')
Tracker.alias(int, 'aip', 'anonymizeIp', 'anonIp', 'anonymize-ip')
Tracker.alias(safe_unicode, 'ds', 'dataSource', 'data-source')
# Campaign attribution
Tracker.alias(safe_unicode, 'cn', 'campaign', 'campaignName', 'campaign-name')
Tracker.alias(safe_unicode, 'cs', 'source', 'campaignSource', 'campaign-source')
Tracker.alias(safe_unicode, 'cm', 'medium', 'campaignMedium', 'campaign-medium')
Tracker.alias(safe_unicode, 'ck', 'keyword', 'campaignKeyword', 'campaign-keyword')
Tracker.alias(safe_unicode, 'cc', 'content', 'campaignContent', 'campaign-content')
Tracker.alias(safe_unicode, 'ci', 'campaignId', 'campaignID', 'campaign-id')
# Technical specs
Tracker.alias(safe_unicode, 'sr', 'screenResolution', 'screen-resolution', 'resolution')
Tracker.alias(safe_unicode, 'vp', 'viewport', 'viewportSize', 'viewport-size')
Tracker.alias(safe_unicode, 'de', 'encoding', 'documentEncoding', 'document-encoding')
Tracker.alias(int, 'sd', 'colors', 'screenColors', 'screen-colors')
Tracker.alias(safe_unicode, 'ul', 'language', 'user-language', 'userLanguage')
# Mobile app
Tracker.alias(safe_unicode, 'an', 'appName', 'app-name', 'app')
Tracker.alias(safe_unicode, 'cd', 'contentDescription', 'screenName', 'screen-name', 'content-description')
Tracker.alias(safe_unicode, 'av', 'appVersion', 'app-version', 'version')
Tracker.alias(safe_unicode, 'aid', 'appID', 'appId', 'application-id', 'app-id', 'applicationId')
Tracker.alias(safe_unicode, 'aiid', 'appInstallerId', 'app-installer-id')
# Ecommerce
Tracker.alias(safe_unicode, 'ta', 'affiliation', 'transactionAffiliation', 'transaction-affiliation')
Tracker.alias(safe_unicode, 'ti', 'transaction', 'transactionId', 'transaction-id')
Tracker.alias(float, 'tr', 'revenue', 'transactionRevenue', 'transaction-revenue')
Tracker.alias(float, 'ts', 'shipping', 'transactionShipping', 'transaction-shipping')
Tracker.alias(float, 'tt', 'tax', 'transactionTax', 'transaction-tax')
Tracker.alias(safe_unicode, 'cu', 'currency', 'transactionCurrency',
'transaction-currency') # Currency code, e.g. USD, EUR
Tracker.alias(safe_unicode, 'in', 'item-name', 'itemName')
Tracker.alias(float, 'ip', 'item-price', 'itemPrice')
Tracker.alias(float, 'iq', 'item-quantity', 'itemQuantity')
Tracker.alias(safe_unicode, 'ic', 'item-code', 'sku', 'itemCode')
Tracker.alias(safe_unicode, 'iv', 'item-variation', 'item-category', 'itemCategory', 'itemVariation')
# Events
Tracker.alias(safe_unicode, 'ec', 'event-category', 'eventCategory', 'category')
Tracker.alias(safe_unicode, 'ea', 'event-action', 'eventAction', 'action')
Tracker.alias(safe_unicode, 'el', 'event-label', 'eventLabel', 'label')
Tracker.alias(int, 'ev', 'event-value', 'eventValue', 'value')
Tracker.alias(int, 'ni', 'noninteractive', 'nonInteractive', 'noninteraction', 'nonInteraction')
# Social
Tracker.alias(safe_unicode, 'sa', 'social-action', 'socialAction')
Tracker.alias(safe_unicode, 'sn', 'social-network', 'socialNetwork')
Tracker.alias(safe_unicode, 'st', 'social-target', 'socialTarget')
# Exceptions
Tracker.alias(safe_unicode, 'exd', 'exception-description', 'exceptionDescription', 'exDescription')
Tracker.alias(int, 'exf', 'exception-fatal', 'exceptionFatal', 'exFatal')
# User Timing
Tracker.alias(safe_unicode, 'utc', 'timingCategory', 'timing-category')
Tracker.alias(safe_unicode, 'utv', 'timingVariable', 'timing-variable')
Tracker.alias(float, 'utt', 'time', 'timingTime', 'timing-time')
Tracker.alias(safe_unicode, 'utl', 'timingLabel', 'timing-label')
Tracker.alias(float, 'dns', 'timingDNS', 'timing-dns')
Tracker.alias(float, 'pdt', 'timingPageLoad', 'timing-page-load')
Tracker.alias(float, 'rrt', 'timingRedirect', 'timing-redirect')
Tracker.alias(safe_unicode, 'tcp', 'timingTCPConnect', 'timing-tcp-connect')
Tracker.alias(safe_unicode, 'srt', 'timingServerResponse', 'timing-server-response')
# Custom dimensions and metrics
for i in range(0, 200):
Tracker.alias(safe_unicode, 'cd{0}'.format(i), 'dimension{0}'.format(i))
Tracker.alias(int, 'cm{0}'.format(i), 'metric{0}'.format(i))
# Content groups
for i in range(0, 5):
Tracker.alias(safe_unicode, 'cg{0}'.format(i), 'contentGroup{0}'.format(i))
# Enhanced Ecommerce
Tracker.alias(str, 'pa') # Product action
Tracker.alias(str, 'tcc') # Coupon code
Tracker.alias(unicode, 'pal') # Product action list
Tracker.alias(int, 'cos') # Checkout step
Tracker.alias(str, 'col') # Checkout step option
Tracker.alias(str, 'promoa') # Promotion action
for product_index in range(1, MAX_EC_PRODUCTS):
Tracker.alias(str, 'pr{0}id'.format(product_index)) # Product SKU
Tracker.alias(unicode, 'pr{0}nm'.format(product_index)) # Product name
Tracker.alias(unicode, 'pr{0}br'.format(product_index)) # Product brand
Tracker.alias(unicode, 'pr{0}ca'.format(product_index)) # Product category
Tracker.alias(unicode, 'pr{0}va'.format(product_index)) # Product variant
Tracker.alias(str, 'pr{0}pr'.format(product_index)) # Product price
Tracker.alias(int, 'pr{0}qt'.format(product_index)) # Product quantity
Tracker.alias(str, 'pr{0}cc'.format(product_index)) # Product coupon code
Tracker.alias(int, 'pr{0}ps'.format(product_index)) # Product position
for custom_index in range(MAX_CUSTOM_DEFINITIONS):
Tracker.alias(str, 'pr{0}cd{1}'.format(product_index, custom_index)) # Product custom dimension
Tracker.alias(int, 'pr{0}cm{1}'.format(product_index, custom_index)) # Product custom metric
for list_index in range(1, MAX_EC_LISTS):
Tracker.alias(str, 'il{0}pi{1}id'.format(list_index, product_index)) # Product impression SKU
Tracker.alias(unicode, 'il{0}pi{1}nm'.format(list_index, product_index)) # Product impression name
Tracker.alias(unicode, 'il{0}pi{1}br'.format(list_index, product_index)) # Product impression brand
Tracker.alias(unicode, 'il{0}pi{1}ca'.format(list_index, product_index)) # Product impression category
Tracker.alias(unicode, 'il{0}pi{1}va'.format(list_index, product_index)) # Product impression variant
Tracker.alias(int, 'il{0}pi{1}ps'.format(list_index, product_index)) # Product impression position
Tracker.alias(int, 'il{0}pi{1}pr'.format(list_index, product_index)) # Product impression price
for custom_index in range(MAX_CUSTOM_DEFINITIONS):
Tracker.alias(str, 'il{0}pi{1}cd{2}'.format(list_index, product_index,
custom_index)) # Product impression custom dimension
Tracker.alias(int, 'il{0}pi{1}cm{2}'.format(list_index, product_index,
custom_index)) # Product impression custom metric
for list_index in range(1, MAX_EC_LISTS):
Tracker.alias(unicode, 'il{0}nm'.format(list_index)) # Product impression list name
for promotion_index in range(1, MAX_EC_PROMOTIONS):
Tracker.alias(str, 'promo{0}id'.format(promotion_index)) # Promotion ID
Tracker.alias(unicode, 'promo{0}nm'.format(promotion_index)) # Promotion name
Tracker.alias(str, 'promo{0}cr'.format(promotion_index)) # Promotion creative
Tracker.alias(str, 'promo{0}ps'.format(promotion_index)) # Promotion position
# Shortcut for creating trackers
def create(account, *args, **kwargs):
return Tracker(account, *args, **kwargs)
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4

View File

@@ -0,0 +1 @@
import Tracker

View File

@@ -15,7 +15,6 @@
import os import os
from Queue import Queue from Queue import Queue
import shutil
import sqlite3 import sqlite3
import sys import sys
import subprocess import subprocess
@@ -33,6 +32,7 @@ except ImportError:
import cherrypy import cherrypy
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
from UniversalAnalytics import Tracker
import activity_handler import activity_handler
import activity_pinger import activity_pinger
@@ -55,6 +55,7 @@ ARGS = None
SIGNAL = None SIGNAL = None
SYS_PLATFORM = None SYS_PLATFORM = None
SYS_LANGUAGE = None
SYS_ENCODING = None SYS_ENCODING = None
QUIET = False QUIET = False
@@ -72,6 +73,7 @@ NOTIFY_QUEUE = Queue()
INIT_LOCK = threading.Lock() INIT_LOCK = threading.Lock()
_INITIALIZED = False _INITIALIZED = False
_STARTED = False _STARTED = False
_UPDATE = False
DATA_DIR = None DATA_DIR = None
@@ -85,6 +87,7 @@ CURRENT_VERSION = None
LATEST_VERSION = None LATEST_VERSION = None
COMMITS_BEHIND = None COMMITS_BEHIND = None
PREV_RELEASE = None PREV_RELEASE = None
LATEST_RELEASE = None
UMASK = None UMASK = None
@@ -95,6 +98,8 @@ DEV = False
WS_CONNECTED = False WS_CONNECTED = False
PLEX_SERVER_UP = None PLEX_SERVER_UP = None
TRACKER = None
def initialize(config_file): def initialize(config_file):
with INIT_LOCK: with INIT_LOCK:
@@ -106,6 +111,7 @@ def initialize(config_file):
global LATEST_VERSION global LATEST_VERSION
global PREV_RELEASE global PREV_RELEASE
global UMASK global UMASK
global _UPDATE
CONFIG = plexpy.config.Config(config_file) CONFIG = plexpy.config.Config(config_file)
CONFIG_FILE = config_file CONFIG_FILE = config_file
@@ -249,6 +255,7 @@ def initialize(config_file):
if common.VERSION_NUMBER != PREV_RELEASE: if common.VERSION_NUMBER != PREV_RELEASE:
CONFIG.UPDATE_SHOW_CHANGELOG = 1 CONFIG.UPDATE_SHOW_CHANGELOG = 1
CONFIG.write() CONFIG.write()
_UPDATE = True
# Write current release version to file for update checking # Write current release version to file for update checking
try: try:
@@ -454,6 +461,19 @@ def start():
if CONFIG.FIRST_RUN_COMPLETE: if CONFIG.FIRST_RUN_COMPLETE:
activity_pinger.connect_server(log=True, startup=True) activity_pinger.connect_server(log=True, startup=True)
if CONFIG.SYSTEM_ANALYTICS:
global TRACKER
TRACKER = initialize_tracker()
# Send system analytics events
if not CONFIG.FIRST_RUN_COMPLETE:
analytics_event(category='system', action='install')
elif _UPDATE:
analytics_event(category='system', action='update')
analytics_event(category='system', action='start')
_STARTED = True _STARTED = True
@@ -1667,3 +1687,38 @@ def shutdown(restart=False, update=False, checkout=False):
def generate_uuid(): def generate_uuid():
return uuid.uuid4().hex return uuid.uuid4().hex
def initialize_tracker():
data = {
'dataSource': 'server',
'appName': 'Tautulli',
'appVersion': common.VERSION_NUMBER,
'appId': plexpy.INSTALL_TYPE,
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_VERSION), # App Platform
'userLanguage': plexpy.SYS_LANGUAGE,
'documentEncoding': plexpy.SYS_ENCODING,
'noninteractive': True
}
tracker = Tracker.create('UA-111522699-2', client_id=CONFIG.PMS_UUID, hash_client_id=True)
tracker.set(data)
return tracker
def analytics_event(category, action, label=None, value=None, **kwargs):
data = {'category': category, 'action': action}
if label is not None:
data['label'] = label
if value is not None:
data['value'] = value
if kwargs:
data.update(kwargs)
if TRACKER:
TRACKER.send('event', data)

View File

@@ -469,28 +469,29 @@ NOTIFICATION_PARAMETERS = [
{ {
'category': 'Plex Update Available', 'category': 'Plex Update Available',
'parameters': [ 'parameters': [
{'name': 'Update Version', 'type': 'int', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'}, {'name': 'Update Version', 'type': 'str', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'},
{'name': 'Update Url', 'type': 'int', 'value': 'update_url', 'description': 'The download URL for the available update.'}, {'name': 'Update Url', 'type': 'str', 'value': 'update_url', 'description': 'The download URL for the available update.'},
{'name': 'Update Release Date', 'type': 'int', 'value': 'update_release_date', 'description': 'The release date of the available update.'}, {'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', 'description': 'The release date of the available update.'},
{'name': 'Update Channel', 'type': 'int', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'}, {'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'},
{'name': 'Update Platform', 'type': 'int', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'}, {'name': 'Update Platform', 'type': 'str', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'},
{'name': 'Update Distro', 'type': 'int', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'}, {'name': 'Update Distro', 'type': 'str', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'},
{'name': 'Update Distro Build', 'type': 'int', 'value': 'update_distro_build', 'description': 'The distro build of your Plex Server.'}, {'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build', 'description': 'The distro build of your Plex Server.'},
{'name': 'Update Requirements', 'type': 'int', 'value': 'update_requirements', 'description': 'The requirements for the available update.'}, {'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements', 'description': 'The requirements for the available update.'},
{'name': 'Update Extra Info', 'type': 'int', 'value': 'update_extra_info', 'description': 'Any extra info for the available update.'}, {'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info', 'description': 'Any extra info for the available update.'},
{'name': 'Update Changelog Added', 'type': 'int', 'value': 'update_changelog_added', 'description': 'The added changelog for the available update.'}, {'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added', 'description': 'The added changelog for the available update.'},
{'name': 'Update Changelog Fixed', 'type': 'int', 'value': 'update_changelog_fixed', 'description': 'The fixed changelog for the available update.'}, {'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed', 'description': 'The fixed changelog for the available update.'},
] ]
}, },
{ {
'category': 'Tautulli Update Available', 'category': 'Tautulli Update Available',
'parameters': [ 'parameters': [
{'name': 'Tautulli Update Version', 'type': 'int', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'}, {'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'},
{'name': 'Tautulli Update Tar', 'type': 'int', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'}, {'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url', 'description': 'The release page URL on GitHub'},
{'name': 'Tautulli Update Zip', 'type': 'int', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'}, {'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'},
{'name': 'Tautulli Update Commit', 'type': 'int', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'}, {'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'},
{'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'},
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'}, {'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'},
{'name': 'Tautulli Update Changelog', 'type': 'int', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'}, {'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'},
] ]
}, },
] ]

View File

@@ -615,6 +615,7 @@ _CONFIG_DEFINITIONS = {
'XBMC_ON_CONCURRENT': (int, 'XBMC', 0), 'XBMC_ON_CONCURRENT': (int, 'XBMC', 0),
'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0), 'XBMC_ON_NEWDEVICE': (int, 'XBMC', 0),
'JWT_SECRET': (str, 'Advanced', ''), 'JWT_SECRET': (str, 'Advanced', ''),
'SYSTEM_ANALYTICS': (int, 'Advanced', 1)
} }
_BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID', '_HOOK'] _BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID', '_HOOK']

View File

@@ -27,7 +27,7 @@ class Graphs(object):
def __init__(self): def __init__(self):
pass pass
def get_total_plays_per_day(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_per_day(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -38,17 +38,22 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \ query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY date_played ' \ 'GROUP BY date_played ' \
'ORDER BY started ASC' % (time_range, user_cond) 'ORDER BY started ASC' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -60,7 +65,7 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \ 'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY date_played ' \ 'GROUP BY date_played ' \
'ORDER BY started ASC' % (time_range, user_cond) 'ORDER BY started ASC' % (time_range, user_cond)
@@ -111,7 +116,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_per_dayofweek(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_per_dayofweek(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -122,7 +127,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT strftime("%%w", datetime(started, "unixepoch", "localtime")) AS daynumber, ' \ query = 'SELECT strftime("%%w", datetime(started, "unixepoch", "localtime")) AS daynumber, ' \
@@ -137,10 +147,10 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY dayofweek ' \ 'GROUP BY dayofweek ' \
'ORDER BY daynumber' % (time_range, user_cond) 'ORDER BY daynumber' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -160,7 +170,7 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \ 'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY dayofweek ' \ 'GROUP BY dayofweek ' \
'ORDER BY daynumber' % (time_range, user_cond) 'ORDER BY daynumber' % (time_range, user_cond)
@@ -212,7 +222,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_per_hourofday(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_per_hourofday(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -223,17 +233,22 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT strftime("%%H", datetime(started, "unixepoch", "localtime")) AS hourofday, ' \ query = 'SELECT strftime("%%H", datetime(started, "unixepoch", "localtime")) AS hourofday, ' \
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY hourofday ' \ 'GROUP BY hourofday ' \
'ORDER BY hourofday' % (time_range, user_cond) 'ORDER BY hourofday' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -245,7 +260,7 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \ 'SUM(CASE WHEN media_type = "track" AND stopped > 0 THEN (stopped - started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY hourofday ' \ 'GROUP BY hourofday ' \
'ORDER BY hourofday' % (time_range, user_cond) 'ORDER BY hourofday' % (time_range, user_cond)
@@ -295,9 +310,9 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None): def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, grouping=None):
import time as time import time as time
if not time_range.isdigit(): if not time_range.isdigit():
time_range = '12' time_range = '12'
@@ -308,17 +323,22 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) AS datestring, ' \ query = 'SELECT strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) AS datestring, ' \
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \ 'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \ 'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \ 'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
'ORDER BY datestring DESC LIMIT %s' % (time_range, user_cond, time_range) 'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -384,7 +404,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -395,7 +415,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT platform, ' \ query = 'SELECT platform, ' \
@@ -403,11 +428,11 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \
'COUNT(id) AS total_count ' \ 'COUNT(id) AS total_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
'GROUP BY platform ' \ 'GROUP BY platform ' \
'ORDER BY total_count DESC ' \ 'ORDER BY total_count DESC ' \
'LIMIT 10' % (time_range, user_cond) 'LIMIT 10' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -421,7 +446,7 @@ class Graphs(object):
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \ 'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
'GROUP BY platform ' \ 'GROUP BY platform ' \
'ORDER BY total_duration DESC ' \ 'ORDER BY total_duration DESC ' \
'LIMIT 10' % (time_range, user_cond) 'LIMIT 10' % (time_range, user_cond)
@@ -453,7 +478,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -464,7 +489,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT ' \ query = 'SELECT ' \
@@ -475,12 +505,12 @@ class Graphs(object):
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \ 'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \ 'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count, ' \
'COUNT(session_history.id) AS total_count ' \ 'COUNT(session_history.id) AS total_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'JOIN users ON session_history.user_id = users.user_id ' \ 'JOIN users ON session_history.user_id = users.user_id ' \
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
'GROUP BY session_history.user_id ' \ 'GROUP BY session_history.user_id ' \
'ORDER BY total_count DESC ' \ 'ORDER BY total_count DESC ' \
'LIMIT 10' % (time_range, user_cond) 'LIMIT 10' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -498,7 +528,7 @@ class Graphs(object):
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
'FROM session_history ' \ 'FROM session_history ' \
'JOIN users ON session_history.user_id = users.user_id ' \ 'JOIN users ON session_history.user_id = users.user_id ' \
'WHERE (datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime")) %s' \
'GROUP BY session_history.user_id ' \ 'GROUP BY session_history.user_id ' \
'ORDER BY total_duration DESC ' \ 'ORDER BY total_duration DESC ' \
'LIMIT 10' % (time_range, user_cond) 'LIMIT 10' % (time_range, user_cond)
@@ -535,7 +565,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_per_stream_type(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_per_stream_type(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -546,7 +576,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT date(session_history.started, "unixepoch", "localtime") AS date_played, ' \ query = 'SELECT date(session_history.started, "unixepoch", "localtime") AS date_played, ' \
@@ -556,14 +591,15 @@ class Graphs(object):
'THEN 1 ELSE 0 END) AS ds_count, ' \ 'THEN 1 ELSE 0 END) AS ds_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count ' \ 'THEN 1 ELSE 0 END) AS tc_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \ 'datetime("now", "-%s days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \ '(session_history.media_type = "episode" OR ' \
'session_history.media_type = "movie" OR ' \
'session_history.media_type = "track") %s' \ 'session_history.media_type = "track") %s' \
'GROUP BY date_played ' \ 'GROUP BY date_played ' \
'ORDER BY started ASC' % (time_range, user_cond) 'ORDER BY started ASC' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -579,7 +615,7 @@ class Graphs(object):
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS tc_count ' \
'FROM session_history ' \ 'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") AND ' \ 'datetime("now", "-%s days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \ '(session_history.media_type = "episode" OR session_history.media_type = "movie" OR ' \
'session_history.media_type = "track") %s' \ 'session_history.media_type = "track") %s' \
@@ -633,7 +669,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -644,7 +680,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT session_history_media_info.video_resolution AS resolution, ' \ query = 'SELECT session_history_media_info.video_resolution AS resolution, ' \
@@ -655,14 +696,14 @@ class Graphs(object):
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count, ' \ 'THEN 1 ELSE 0 END) AS tc_count, ' \
'COUNT(session_history.id) AS total_count ' \ 'COUNT(session_history.id) AS total_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \ 'datetime("now", "-%s days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \ '(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
'GROUP BY resolution ' \ 'GROUP BY resolution ' \
'ORDER BY total_count DESC ' \ 'ORDER BY total_count DESC ' \
'LIMIT 10' % (time_range, user_cond) 'LIMIT 10' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -680,7 +721,7 @@ class Graphs(object):
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
'FROM session_history ' \ 'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \ 'datetime("now", "-%s days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \ '(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
'GROUP BY resolution ' \ 'GROUP BY resolution ' \
@@ -718,7 +759,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_total_plays_by_stream_resolution(self, time_range='30', y_axis='plays', user_id=None): def get_total_plays_by_stream_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -729,7 +770,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT ' \ query = 'SELECT ' \
@@ -752,14 +798,14 @@ class Graphs(object):
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\
'THEN 1 ELSE 0 END) AS tc_count, ' \ 'THEN 1 ELSE 0 END) AS tc_count, ' \
'COUNT(session_history.id) AS total_count ' \ 'COUNT(session_history.id) AS total_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \ 'datetime("now", "-%s days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \ '(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
'GROUP BY resolution ' \ 'GROUP BY resolution ' \
'ORDER BY total_count DESC ' \ 'ORDER BY total_count DESC ' \
'LIMIT 10' % (time_range, user_cond) 'LIMIT 10' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -789,7 +835,7 @@ class Graphs(object):
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
'FROM session_history ' \ 'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \ 'datetime("now", "-%s days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \ '(session_history.media_type = "episode" OR session_history.media_type = "movie") %s' \
'GROUP BY resolution ' \ 'GROUP BY resolution ' \
@@ -827,7 +873,7 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]} 'series': [series_1_output, series_2_output, series_3_output]}
return output return output
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None): def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -838,7 +884,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT session_history.platform AS platform, ' \ query = 'SELECT session_history.platform AS platform, ' \
@@ -849,13 +900,15 @@ class Graphs(object):
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count, ' \ 'THEN 1 ELSE 0 END) AS tc_count, ' \
'COUNT(session_history.id) AS total_count ' \ 'COUNT(session_history.id) AS total_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \ 'WHERE datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") AND ' \ 'datetime("now", "-%s days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \ '(session_history.media_type = "episode" OR ' \
'session_history.media_type = "movie" OR ' \
'session_history.media_type = "track") %s' \
'GROUP BY platform ' \ 'GROUP BY platform ' \
'ORDER BY total_count DESC LIMIT 10' % (time_range, user_cond) 'ORDER BY total_count DESC LIMIT 10' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -874,9 +927,11 @@ class Graphs(object):
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS total_duration ' \
'FROM session_history ' \ 'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \ 'WHERE datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") AND ' \ 'datetime("now", "-%s days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \ '(session_history.media_type = "episode" OR ' \
'session_history.media_type = "movie" OR ' \
'session_history.media_type = "track") %s' \
'GROUP BY platform ' \ 'GROUP BY platform ' \
'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond) 'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond)
@@ -908,7 +963,7 @@ class Graphs(object):
return output return output
def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None): def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
@@ -919,7 +974,12 @@ class Graphs(object):
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
elif user_id and user_id.isdigit(): elif user_id and user_id.isdigit():
user_cond = 'AND session_history.user_id = %s ' % user_id user_cond = 'AND session_history.user_id = %s ' % user_id
if grouping is None:
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
group_by = 'reference_id' if grouping else 'id'
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT ' \ query = 'SELECT ' \
@@ -933,14 +993,16 @@ class Graphs(object):
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count, ' \ 'THEN 1 ELSE 0 END) AS tc_count, ' \
'COUNT(session_history.id) AS total_count ' \ 'COUNT(session_history.id) AS total_count ' \
'FROM session_history ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'JOIN users ON session_history.user_id = users.user_id ' \ 'JOIN users ON session_history.user_id = users.user_id ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \ 'WHERE datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") AND ' \ 'datetime("now", "-%s days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \ '(session_history.media_type = "episode" OR ' \
'session_history.media_type = "movie" OR ' \
'session_history.media_type = "track") %s' \
'GROUP BY username ' \ 'GROUP BY username ' \
'ORDER BY total_count DESC LIMIT 10' % (time_range, user_cond) 'ORDER BY total_count DESC LIMIT 10' % (group_by, time_range, user_cond)
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
@@ -963,9 +1025,11 @@ class Graphs(object):
'FROM session_history ' \ 'FROM session_history ' \
'JOIN users ON session_history.user_id = users.user_id ' \ 'JOIN users ON session_history.user_id = users.user_id ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \ 'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \ 'WHERE datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") AND ' \ 'datetime("now", "-%s days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") %s' \ '(session_history.media_type = "episode" OR ' \
'session_history.media_type = "movie" OR ' \
'session_history.media_type = "track") %s' \
'GROUP BY username ' \ 'GROUP BY username ' \
'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond) 'ORDER BY total_duration DESC LIMIT 10' % (time_range, user_cond)

View File

@@ -907,6 +907,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
'update_changelog_fixed': pms_download_info['changelog_fixed'], 'update_changelog_fixed': pms_download_info['changelog_fixed'],
# Tautulli update parameters # Tautulli update parameters
'tautulli_update_version': plexpy_download_info['tag_name'], 'tautulli_update_version': plexpy_download_info['tag_name'],
'tautulli_update_release_url': plexpy_download_info['html_url'],
'tautulli_update_tar': plexpy_download_info['tarball_url'], 'tautulli_update_tar': plexpy_download_info['tarball_url'],
'tautulli_update_zip': plexpy_download_info['zipball_url'], 'tautulli_update_zip': plexpy_download_info['zipball_url'],
'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''), 'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),

View File

@@ -417,7 +417,7 @@ def get_notifiers(notifier_id=None, notify_action=None):
db = database.MonitorDatabase() db = database.MonitorDatabase()
result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s' result = db.select('SELECT id, agent_id, agent_name, agent_label, friendly_name, %s FROM notifiers %s'
% (', '.join(notify_actions), where), args=args) % (', '.join(notify_actions), where), args=args)
for item in result: for item in result:
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions])) item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
@@ -892,26 +892,30 @@ class ANDROIDAPP(Notifier):
if not CRYPTODOME: if not CRYPTODOME:
config_option.append({ config_option.append({
'label': 'Warning', 'label': 'Warning',
'description': '<strong>The PyCryptodome library is missing. ' \ 'description': '<strong>The PyCryptodome library is missing. '
'The content of your notifications will be sent unencrypted!</strong><br>' \ 'The content of your notifications will be sent unencrypted!</strong><br>'
'Please install the library to encrypt the notification contents. ' \ 'Please install the library to encrypt the notification contents. '
'Instructions can be found in the ' \ 'Instructions can be found in the '
'<a href="' + helpers.anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome' '<a href="' + helpers.anon_url(
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.', 'https://github.com/%s/%s-Wiki/wiki/'
'Frequently-Asked-Questions#notifications-pycryptodome'
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) +
'" target="_blank">FAQ</a>.',
'input_type': 'help' 'input_type': 'help'
}) })
else: else:
config_option.append({ config_option.append({
'label': 'Note', 'label': 'Note',
'description': 'The PyCryptodome library was found. ' \ 'description': 'The PyCryptodome library was found. '
'The content of your notifications will be sent encrypted!', 'The content of your notifications will be sent encrypted!',
'input_type': 'help' 'input_type': 'help'
}) })
config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \ config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \
'<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \ '<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \
'OneSignal</a> API. Some user data is collected and cannot be encrypted. ' \ 'OneSignal</a> API. Some user data is collected and cannot be encrypted. ' \
'Please read the <a href="' + helpers.anon_url('https://onesignal.com/privacy_policy') + '" target="_blank">' \ 'Please read the <a href="' + helpers.anon_url(
'https://onesignal.com/privacy_policy') + '" target="_blank">' \
'OneSignal Privacy Policy</a> for more details.' 'OneSignal Privacy Policy</a> for more details.'
devices = self.get_devices() devices = self.get_devices()
@@ -919,9 +923,9 @@ class ANDROIDAPP(Notifier):
if not devices: if not devices:
config_option.append({ config_option.append({
'label': 'Device', 'label': 'Device',
'description': 'No devices registered. ' \ 'description': 'No devices registered. '
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \ '<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" '
'style="cursor: pointer;">Get the Android App</a> and register a device.', 'style="cursor: pointer;">Get the Android App</a> and register a device.',
'input_type': 'help' 'input_type': 'help'
}) })
else: else:
@@ -929,9 +933,9 @@ class ANDROIDAPP(Notifier):
'label': 'Device', 'label': 'Device',
'value': self.config['device_id'], 'value': self.config['device_id'],
'name': 'androidapp_device_id', 'name': 'androidapp_device_id',
'description': 'Set your Android app device or ' \ 'description': 'Set your Android app device or '
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" ' \ '<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" '
'style="cursor: pointer;">register a new device</a> with Tautulli.', 'style="cursor: pointer;">register a new device</a> with Tautulli.',
'input_type': 'select', 'input_type': 'select',
'select_options': devices 'select_options': devices
}) })
@@ -1184,7 +1188,7 @@ class DISCORD(Notifier):
'value': self.config['incl_card'], 'value': self.config['incl_card'],
'name': 'discord_incl_card', 'name': 'discord_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>' 'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Imgur upload may need to be enabled under the notifications settings tab.', 'Note: Imgur upload may need to be enabled under the notifications settings tab.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Include Plot Summaries', {'label': 'Include Plot Summaries',
@@ -1209,7 +1213,7 @@ class DISCORD(Notifier):
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'discord_movie_provider', 'name': 'discord_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -1217,7 +1221,7 @@ class DISCORD(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'discord_tv_provider', 'name': 'discord_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -1557,14 +1561,14 @@ class FACEBOOK(Notifier):
'value': self.config['incl_card'], 'value': self.config['incl_card'],
'name': 'facebook_incl_card', 'name': 'facebook_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>' 'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Imgur upload may need to be enabled under the notifications settings tab.', 'Note: Imgur upload may need to be enabled under the notifications settings tab.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'facebook_movie_provider', 'name': 'facebook_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -1572,7 +1576,7 @@ class FACEBOOK(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'facebook_tv_provider', 'name': 'facebook_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -1616,6 +1620,7 @@ class GROUPME(Notifier):
poster_content = result[0] poster_content = result[0]
else: else:
poster_content = '' poster_content = ''
logger.error(u"Tautulli Notifiers :: Unable to retrieve image for {name}.".format(name=self.NAME))
if poster_content: if poster_content:
headers = {'X-Access-Token': self.config['access_token'], headers = {'X-Access-Token': self.config['access_token'],
@@ -1629,9 +1634,9 @@ class GROUPME(Notifier):
data['attachments'] = [{'type': 'image', data['attachments'] = [{'type': 'image',
'url': r_content['payload']['picture_url']}] 'url': r_content['payload']['picture_url']}]
else: else:
logger.error(u"Tautulli Notifiers :: {name} poster failed: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r)) logger.error(u"Tautulli Notifiers :: {name} poster failed: "
u"[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True))) logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
return False
return self.make_request('https://api.groupme.com/v3/bots/post', json=data) return self.make_request('https://api.groupme.com/v3/bots/post', json=data)
@@ -1835,7 +1840,7 @@ class HIPCHAT(Notifier):
headers = {'Content-type': 'application/json'} headers = {'Content-type': 'application/json'}
return self.make_request(self.config['hook'], json=data) return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Hipchat Custom Integrations Full URL', config_option = [{'label': 'Hipchat Custom Integrations Full URL',
@@ -1877,7 +1882,7 @@ class HIPCHAT(Notifier):
'value': self.config['incl_card'], 'value': self.config['incl_card'],
'name': 'hipchat_incl_card', 'name': 'hipchat_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>' 'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Imgur upload may need to be enabled under the notifications settings tab.<br>' 'Note: Imgur upload may need to be enabled under the notifications settings tab.<br>'
'Note: This will change the notification type to HTML and emoticons will no longer work.', 'Note: This will change the notification type to HTML and emoticons will no longer work.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
@@ -1897,7 +1902,7 @@ class HIPCHAT(Notifier):
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'hipchat_movie_provider', 'name': 'hipchat_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -1905,7 +1910,7 @@ class HIPCHAT(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'hipchat_tv_provider', 'name': 'hipchat_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -2090,14 +2095,14 @@ class JOIN(Notifier):
'value': self.config['incl_poster'], 'value': self.config['incl_poster'],
'name': 'join_incl_poster', 'name': 'join_incl_poster',
'description': 'Include a poster with the notifications.<br>' 'description': 'Include a poster with the notifications.<br>'
'Imgur upload may need to be enabled under the notifications settings tab.', 'Note: Imgur upload may need to be enabled under the notifications settings tab.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'join_movie_provider', 'name': 'join_movie_provider',
'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -2105,7 +2110,7 @@ class JOIN(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'join_tv_provider', 'name': 'join_tv_provider',
'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -2559,31 +2564,68 @@ class PUSHBULLET(Notifier):
NAME = 'Pushbullet' NAME = 'Pushbullet'
_DEFAULT_CONFIG = {'api_key': '', _DEFAULT_CONFIG = {'api_key': '',
'device_id': '', 'device_id': '',
'channel_tag': '' 'channel_tag': '',
'incl_subject': 1,
'incl_poster': 0
} }
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'type': 'note', data = {'type': 'note',
'title': subject.encode("utf-8"),
'body': body.encode("utf-8")} 'body': body.encode("utf-8")}
headers = {'Content-type': 'application/json',
'Access-Token': self.config['api_key']
}
if self.config['incl_subject']:
data['title'] = subject.encode("utf-8")
# Can only send to a device or channel, not both. # Can only send to a device or channel, not both.
if self.config['device_id']: if self.config['device_id']:
data['device_iden'] = self.config['device_id'] data['device_iden'] = self.config['device_id']
elif self.config['channel_tag']: elif self.config['channel_tag']:
data['channel_tag'] = self.config['channel_tag'] data['channel_tag'] = self.config['channel_tag']
headers = {'Content-type': 'application/json', if self.config['incl_poster'] and kwargs.get('parameters', {}).get('media_type'):
'Access-Token': self.config['api_key'] # Grab formatted metadata
} pretty_metadata = PrettyMetadata(kwargs['parameters'])
# Retrieve the poster from Plex
result = pmsconnect.PmsConnect().get_image(img=pretty_metadata.parameters.get('poster_thumb', ''))
if result and result[0]:
poster_content = result[0]
else:
poster_content = ''
logger.error(u"Tautulli Notifiers :: Unable to retrieve image for {name}.".format(name=self.NAME))
if poster_content:
poster_filename = 'poster_{}.jpg'.format(pretty_metadata.parameters['rating_key'])
file_json = {'file_name': poster_filename, 'file_type': 'image/jpeg'}
files = {'file': (poster_filename, poster_content, 'image/jpeg')}
r = requests.post('https://api.pushbullet.com/v2/upload-request', headers=headers, json=file_json)
file_response = r.json()
upload_url = file_response.pop('upload_url')
r = requests.post(upload_url, files=files)
if r.status_code == 204:
data['type'] = 'file'
file_response.pop('data', None)
data.update(file_response)
else:
logger.error(u"Tautulli Notifiers :: Unable to upload image to {name}: "
u"[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
return self.make_request('https://api.pushbullet.com/v2/pushes', headers=headers, json=data) return self.make_request('https://api.pushbullet.com/v2/pushes', headers=headers, json=data)
def get_devices(self): def get_devices(self):
if self.config['api_key']: if self.config['api_key']:
headers={'Content-type': "application/json", headers = {'Content-type': "application/json",
'Access-Token': self.config['api_key'] 'Access-Token': self.config['api_key']
} }
r = requests.get('https://api.pushbullet.com/v2/devices', headers=headers) r = requests.get('https://api.pushbullet.com/v2/devices', headers=headers)
@@ -2594,7 +2636,8 @@ class PUSHBULLET(Notifier):
devices.update({'': ''}) devices.update({'': ''})
return devices return devices
else: else:
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r)) logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: "
u"[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True))) logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
return {'': ''} return {'': ''}
@@ -2612,8 +2655,8 @@ class PUSHBULLET(Notifier):
{'label': 'Device', {'label': 'Device',
'value': self.config['device_id'], 'value': self.config['device_id'],
'name': 'pushbullet_device_id', 'name': 'pushbullet_device_id',
'description': 'Set your Pushbullet device. If set, will override channel tag. ' \ 'description': 'Set your Pushbullet device. If set, will override channel tag. '
'Leave blank to notify on all devices.', 'Leave blank to notify on all devices.',
'input_type': 'select', 'input_type': 'select',
'select_options': self.get_devices() 'select_options': self.get_devices()
}, },
@@ -2622,6 +2665,18 @@ class PUSHBULLET(Notifier):
'name': 'pushbullet_channel_tag', 'name': 'pushbullet_channel_tag',
'description': 'A channel tag (optional).', 'description': 'A channel tag (optional).',
'input_type': 'text' 'input_type': 'text'
},
{'label': 'Include Subject Line',
'value': self.config['incl_subject'],
'name': 'pushbullet_incl_subject',
'description': 'Include the subject line with the notifications.',
'input_type': 'checkbox'
},
{'label': 'Include Poster Image',
'value': self.config['incl_poster'],
'name': 'pushbullet_incl_poster',
'description': 'Include a poster with the notifications.',
'input_type': 'checkbox'
} }
] ]
@@ -2639,6 +2694,8 @@ class PUSHOVER(Notifier):
'priority': 0, 'priority': 0,
'sound': '', 'sound': '',
'incl_url': 1, 'incl_url': 1,
'incl_subject': 1,
'incl_poster': 0,
'movie_provider': '', 'movie_provider': '',
'tv_provider': '', 'tv_provider': '',
'music_provider': '' 'music_provider': ''
@@ -2647,11 +2704,18 @@ class PUSHOVER(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'token': self.config['api_token'], data = {'token': self.config['api_token'],
'user': self.config['key'], 'user': self.config['key'],
'title': subject.encode("utf-8"),
'message': body.encode("utf-8"), 'message': body.encode("utf-8"),
'sound': self.config['sound'], 'sound': self.config['sound'],
'html': self.config['html_support'], 'html': self.config['html_support'],
'priority': self.config['priority']} 'priority': self.config['priority'],
'timestamp': int(time.time())}
if self.config['incl_subject']:
data['title'] = subject.encode("utf-8")
headers = {'Content-type': 'application/x-www-form-urlencoded'}
files = {}
if self.config['incl_url'] and kwargs.get('parameters', {}).get('media_type'): if self.config['incl_url'] and kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata # Grab formatted metadata
@@ -2672,9 +2736,24 @@ class PUSHOVER(Notifier):
data['url'] = provider_link data['url'] = provider_link
data['url_title'] = caption data['url_title'] = caption
headers = {'Content-type': 'application/x-www-form-urlencoded'} if self.config['incl_poster'] and kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata
pretty_metadata = PrettyMetadata(kwargs['parameters'])
return self.make_request('https://api.pushover.net/1/messages.json', headers=headers, data=data) # Retrieve the poster from Plex
result = pmsconnect.PmsConnect().get_image(img=pretty_metadata.parameters.get('poster_thumb', ''))
if result and result[0]:
poster_content = result[0]
else:
poster_content = ''
logger.error(u"Tautulli Notifiers :: Unable to retrieve image for {name}.".format(name=self.NAME))
if poster_content:
poster_filename = 'poster_{}.jpg'.format(pretty_metadata.parameters['rating_key'])
files = {'attachment': (poster_filename, poster_content, 'image/jpeg')}
headers = {}
return self.make_request('https://api.pushover.net/1/messages.json', headers=headers, data=data, files=files)
def get_sounds(self): def get_sounds(self):
if self.config['api_token']: if self.config['api_token']:
@@ -2688,7 +2767,8 @@ class PUSHOVER(Notifier):
sounds.update({'': ''}) sounds.update({'': ''})
return sounds return sounds
else: else:
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} sounds list: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r)) logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} sounds list: "
u"[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True))) logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
return {'': ''} return {'': ''}
@@ -2735,11 +2815,23 @@ class PUSHOVER(Notifier):
'description': 'Include a supplementary URL with the notifications.', 'description': 'Include a supplementary URL with the notifications.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Include Subject Line',
'value': self.config['incl_subject'],
'name': 'pushover_incl_subject',
'description': 'Include the subject line with the notifications.',
'input_type': 'checkbox'
},
{'label': 'Include Poster Image',
'value': self.config['incl_poster'],
'name': 'pushover_incl_poster',
'description': 'Include a poster with the notifications.',
'input_type': 'checkbox'
},
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'pushover_movie_provider', 'name': 'pushover_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -2747,7 +2839,7 @@ class PUSHOVER(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'pushover_tv_provider', 'name': 'pushover_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -3074,7 +3166,7 @@ class SLACK(Notifier):
'value': self.config['incl_card'], 'value': self.config['incl_card'],
'name': 'slack_incl_card', 'name': 'slack_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>' 'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Imgur upload may need to be enabled under the notifications settings tab.', 'Note: Imgur upload may need to be enabled under the notifications settings tab.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Include Plot Summaries', {'label': 'Include Plot Summaries',
@@ -3099,7 +3191,7 @@ class SLACK(Notifier):
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'slack_movie_provider', 'name': 'slack_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -3107,7 +3199,7 @@ class SLACK(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'slack_tv_provider', 'name': 'slack_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -3144,41 +3236,46 @@ class TELEGRAM(Notifier):
else: else:
text = body.encode('utf-8') text = body.encode('utf-8')
if self.config['incl_poster'] and kwargs.get('parameters'):
parameters = kwargs['parameters']
poster_url = parameters.get('poster_url','')
if poster_url:
poster_data = {'photo': poster_url,
'chat_id': self.config['chat_id'],
'disable_notification': True}
r = requests.post('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']), json=poster_data)
if r.status_code == 200:
logger.info(u"Tautulli Notifiers :: {name} poster sent.".format(name=self.NAME))
else:
logger.error(u"Tautulli Notifiers :: {name} poster failed: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
data['text'] = text
if self.config['html_support']: if self.config['html_support']:
data['parse_mode'] = 'HTML' data['parse_mode'] = 'HTML'
if self.config['incl_poster'] and kwargs.get('parameters'):
# Grab formatted metadata
pretty_metadata = PrettyMetadata(kwargs['parameters'])
# Retrieve the poster from Plex
result = pmsconnect.PmsConnect().get_image(img=pretty_metadata.parameters.get('poster_thumb', ''))
if result and result[0]:
poster_content = result[0]
else:
poster_content = ''
logger.error(u"Tautulli Notifiers :: Unable to retrieve image for {name}.".format(name=self.NAME))
if poster_content:
poster_filename = 'poster_{}.jpg'.format(pretty_metadata.parameters['rating_key'])
files = {'photo': (poster_filename, poster_content, 'image/jpeg')}
data['caption'] = text
return self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
data=data, files=files)
data['text'] = text
if self.config['disable_web_preview']: if self.config['disable_web_preview']:
data['disable_web_page_preview'] = True data['disable_web_page_preview'] = True
headers = {'Content-type': 'application/x-www-form-urlencoded'} headers = {'Content-type': 'application/x-www-form-urlencoded'}
return self.make_request('https://api.telegram.org/bot{}/sendMessage'.format(self.config['bot_token']), headers=headers, data=data) return self.make_request('https://api.telegram.org/bot{}/sendMessage'.format(self.config['bot_token']),
headers=headers, data=data)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Telegram Bot Token', config_option = [{'label': 'Telegram Bot Token',
'value': self.config['bot_token'], 'value': self.config['bot_token'],
'name': 'telegram_bot_token', 'name': 'telegram_bot_token',
'description': 'Your Telegram bot token. ' 'description': 'Your Telegram bot token. '
'Contact <a href="' + helpers.anon_url('https://telegram.me/BotFather') + '" target="_blank">@BotFather</a>' 'Contact <a href="' + helpers.anon_url('https://telegram.me/BotFather') +
'" target="_blank">@BotFather</a>'
' on Telegram to get one.', ' on Telegram to get one.',
'input_type': 'text' 'input_type': 'text'
}, },
@@ -3186,7 +3283,8 @@ class TELEGRAM(Notifier):
'value': self.config['chat_id'], 'value': self.config['chat_id'],
'name': 'telegram_chat_id', 'name': 'telegram_chat_id',
'description': 'Your Telegram Chat ID, Group ID, or @channelusername. ' 'description': 'Your Telegram Chat ID, Group ID, or @channelusername. '
'Contact <a href="' + helpers.anon_url('https://telegram.me/myidbot') + '" target="_blank">@myidbot</a>' 'Contact <a href="' + helpers.anon_url('https://telegram.me/myidbot') +
'" target="_blank">@myidbot</a>'
' on Telegram to get an ID.', ' on Telegram to get an ID.',
'input_type': 'text' 'input_type': 'text'
}, },
@@ -3199,8 +3297,7 @@ class TELEGRAM(Notifier):
{'label': 'Include Poster Image', {'label': 'Include Poster Image',
'value': self.config['incl_poster'], 'value': self.config['incl_poster'],
'name': 'telegram_incl_poster', 'name': 'telegram_incl_poster',
'description': 'Include a poster with the notifications.<br>' 'description': 'Include a poster with the notifications.',
'Imgur upload may need to be enabled under the notifications settings tab.',
'input_type': 'checkbox' 'input_type': 'checkbox'
}, },
{'label': 'Enable HTML Support', {'label': 'Enable HTML Support',
@@ -3310,7 +3407,7 @@ class TWITTER(Notifier):
'value': self.config['incl_poster'], 'value': self.config['incl_poster'],
'name': 'twitter_incl_poster', 'name': 'twitter_incl_poster',
'description': 'Include a poster with the notifications.<br>' 'description': 'Include a poster with the notifications.<br>'
'Imgur upload may need to be enabled under the notifications settings tab.', 'Note: Imgur upload may need to be enabled under the notifications settings tab.',
'input_type': 'checkbox' 'input_type': 'checkbox'
} }
] ]
@@ -3390,7 +3487,7 @@ class XBMC(Notifier):
return False return False
return True return True
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'XBMC Host:Port', config_option = [{'label': 'XBMC Host:Port',
'value': self.config['hosts'], 'value': self.config['hosts'],
@@ -3501,7 +3598,7 @@ class ZAPIER(Notifier):
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'zapier_movie_provider', 'name': 'zapier_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
}, },
@@ -3509,7 +3606,7 @@ class ZAPIER(Notifier):
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'zapier_tv_provider', 'name': 'zapier_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
}, },
@@ -3563,7 +3660,7 @@ def upgrade_config_to_db():
# Get Monitoring config section # Get Monitoring config section
monitoring = plexpy.CONFIG._config['Monitoring'] monitoring = plexpy.CONFIG._config['Monitoring']
# Get the new default notification subject and body text # Get the new default notification subject and body text
defualt_subject_text = {a['name']: a['subject'] for a in available_notification_actions()} defualt_subject_text = {a['name']: a['subject'] for a in available_notification_actions()}
defualt_body_text = {a['name']: a['body'] for a in available_notification_actions()} defualt_body_text = {a['name']: a['body'] for a in available_notification_actions()}
@@ -3575,7 +3672,7 @@ def upgrade_config_to_db():
body_key = 'notify_' + action + '_body_text' body_key = 'notify_' + action + '_body_text'
notify_text[action + '_subject'] = monitoring.get(subject_key, defualt_subject_text[action]) notify_text[action + '_subject'] = monitoring.get(subject_key, defualt_subject_text[action])
notify_text[action + '_body'] = monitoring.get(body_key, defualt_body_text[action]) notify_text[action + '_body'] = monitoring.get(body_key, defualt_body_text[action])
# Check through each notification agent # Check through each notification agent
for agent in get_notify_agents(): for agent in get_notify_agents():
agent_id = AGENT_IDS[agent] agent_id = AGENT_IDS[agent]
@@ -3584,7 +3681,7 @@ def upgrade_config_to_db():
agent_section = section_overrides.get(agent, agent.capitalize()) agent_section = section_overrides.get(agent, agent.capitalize())
agent_config = plexpy.CONFIG._config.get(agent_section) agent_config = plexpy.CONFIG._config.get(agent_section)
agent_config_key = agent_section.lower() agent_config_key = agent_section.lower()
# Make sure there is an existing config section (to prevent adding v2 agents) # Make sure there is an existing config section (to prevent adding v2 agents)
if not agent_config: if not agent_config:
continue continue

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.19-beta" PLEXPY_RELEASE_VERSION = "v2.0.20-beta"

View File

@@ -196,6 +196,8 @@ def checkGithub(auto_update=False):
else: else:
release = releases[0] release = releases[0]
plexpy.LATEST_RELEASE = release['tag_name']
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', 'plexpy_download_info': release, plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', 'plexpy_download_info': release,
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND}) 'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})

View File

@@ -1799,7 +1799,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_date(self, time_range='30', user_id=None, y_axis='plays', **kwargs): def get_plays_by_date(self, time_range='30', user_id=None, y_axis='plays', grouping=None, **kwargs):
""" Get graph data by date. """ Get graph data by date.
``` ```
@@ -1810,6 +1810,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1823,8 +1824,10 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_per_day(time_range=time_range, user_id=user_id, y_axis=y_axis) result = graph.get_total_plays_per_day(time_range=time_range, user_id=user_id, y_axis=y_axis, grouping=grouping)
if result: if result:
return result return result
@@ -1835,7 +1838,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_dayofweek(self, time_range='30', user_id=None, y_axis='plays', **kwargs): def get_plays_by_dayofweek(self, time_range='30', user_id=None, y_axis='plays', grouping=None, **kwargs):
""" Get graph data by day of the week. """ Get graph data by day of the week.
``` ```
@@ -1846,6 +1849,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1859,6 +1863,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_per_dayofweek(time_range=time_range, user_id=user_id, y_axis=y_axis) result = graph.get_total_plays_per_dayofweek(time_range=time_range, user_id=user_id, y_axis=y_axis)
@@ -1871,7 +1877,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_hourofday(self, time_range='30', user_id=None, y_axis='plays', **kwargs): def get_plays_by_hourofday(self, time_range='30', user_id=None, y_axis='plays', grouping=None, **kwargs):
""" Get graph data by hour of the day. """ Get graph data by hour of the day.
``` ```
@@ -1882,6 +1888,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1895,6 +1902,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_per_hourofday(time_range=time_range, user_id=user_id, y_axis=y_axis) result = graph.get_total_plays_per_hourofday(time_range=time_range, user_id=user_id, y_axis=y_axis)
@@ -1907,7 +1916,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, **kwargs): def get_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, grouping=None, **kwargs):
""" Get graph data by month. """ Get graph data by month.
``` ```
@@ -1918,6 +1927,7 @@ class WebInterface(object):
time_range (str): The number of months of data to return time_range (str): The number of months of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1931,6 +1941,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_per_month(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_total_plays_per_month(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -1943,7 +1955,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_plays_by_top_10_platforms(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by top 10 platforms. """ Get graph data by top 10 platforms.
``` ```
@@ -1954,6 +1966,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -1967,6 +1980,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_by_top_10_platforms(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_total_plays_by_top_10_platforms(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -1979,7 +1994,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_plays_by_top_10_users(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by top 10 users. """ Get graph data by top 10 users.
``` ```
@@ -1990,6 +2005,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -2003,6 +2019,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_by_top_10_users(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_total_plays_by_top_10_users(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -2015,7 +2033,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_stream_type(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_plays_by_stream_type(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by stream type by date. """ Get graph data by stream type by date.
``` ```
@@ -2026,6 +2044,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -2039,6 +2058,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_per_stream_type(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_total_plays_per_stream_type(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -2051,7 +2072,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_plays_by_source_resolution(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by source resolution. """ Get graph data by source resolution.
``` ```
@@ -2062,6 +2083,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -2075,6 +2097,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_by_source_resolution(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_total_plays_by_source_resolution(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -2087,7 +2111,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_plays_by_stream_resolution(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_plays_by_stream_resolution(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by stream resolution. """ Get graph data by stream resolution.
``` ```
@@ -2098,6 +2122,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -2111,6 +2136,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_total_plays_by_stream_resolution(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_total_plays_by_stream_resolution(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -2123,7 +2150,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_stream_type_by_top_10_users(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by stream type by top 10 users. """ Get graph data by stream type by top 10 users.
``` ```
@@ -2134,6 +2161,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -2147,6 +2175,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_stream_type_by_top_10_users(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_stream_type_by_top_10_users(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -2159,7 +2189,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, **kwargs): def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', grouping=None, user_id=None, **kwargs):
""" Get graph data by stream type by top 10 platforms. """ Get graph data by stream type by top 10 platforms.
``` ```
@@ -2170,6 +2200,7 @@ class WebInterface(object):
time_range (str): The number of days of data to return time_range (str): The number of days of data to return
y_axis (str): "plays" or "duration" y_axis (str): "plays" or "duration"
user_id (str): The user id to filter the data user_id (str): The user id to filter the data
grouping (int): 0 or 1
Returns: Returns:
json: json:
@@ -2183,6 +2214,8 @@ class WebInterface(object):
} }
``` ```
""" """
grouping = int(grouping) if str(grouping).isdigit() else grouping
graph = graphs.Graphs() graph = graphs.Graphs()
result = graph.get_stream_type_by_top_10_platforms(time_range=time_range, y_axis=y_axis, user_id=user_id) result = graph.get_stream_type_by_top_10_platforms(time_range=time_range, y_axis=y_axis, user_id=user_id)
@@ -2697,8 +2730,8 @@ class WebInterface(object):
reschedule = True reschedule = True
# If we change the SSL setting for PMS or PMS remote setting, make sure we grab the new url. # If we change the SSL setting for PMS or PMS remote setting, make sure we grab the new url.
if kwargs.get('pms_ssl') != plexpy.CONFIG.PMS_SSL or \ if kwargs.get('pms_ssl') != str(plexpy.CONFIG.PMS_SSL) or \
kwargs.get('pms_is_remote') != plexpy.CONFIG.PMS_IS_REMOTE or \ kwargs.get('pms_is_remote') != str(plexpy.CONFIG.PMS_IS_REMOTE) or \
kwargs.get('pms_url_manual') != plexpy.CONFIG.PMS_URL_MANUAL: kwargs.get('pms_url_manual') != plexpy.CONFIG.PMS_URL_MANUAL:
server_changed = True server_changed = True
@@ -3543,14 +3576,30 @@ class WebInterface(object):
if not plexpy.CURRENT_VERSION: if not plexpy.CURRENT_VERSION:
return {'result': 'error', return {'result': 'error',
'message': 'You are running an unknown version of Tautulli.', 'update': None,
'update': None} 'message': 'You are running an unknown version of Tautulli.'
}
elif plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and \ elif plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and \
plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win': plexpy.common.VERSION_NUMBER != plexpy.LATEST_RELEASE:
return {'result': 'success', return {'result': 'success',
'update': True, 'update': True,
'message': 'An update for Tautulli is available.', 'release': True,
'message': 'A new release (%) of Tautulli is available.' % plexpy.LATEST_RELEASE,
'latest_release': plexpy.LATEST_RELEASE,
'release_url': helpers.anon_url(
'https://github.com/%s/%s/releases/tag/%s'
% (plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO,
plexpy.LATEST_RELEASE))
}
elif plexpy.COMMITS_BEHIND > 0 and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and \
plexpy.INSTALL_TYPE != 'win':
return {'result': 'success',
'update': True,
'release': False,
'message': 'A newer version of Tautulli is available.',
'latest_version': plexpy.LATEST_VERSION, 'latest_version': plexpy.LATEST_VERSION,
'commits_behind': plexpy.COMMITS_BEHIND, 'commits_behind': plexpy.COMMITS_BEHIND,
'compare_url': helpers.anon_url( 'compare_url': helpers.anon_url(
@@ -3564,7 +3613,8 @@ class WebInterface(object):
else: else:
return {'result': 'success', return {'result': 'success',
'update': False, 'update': False,
'message': 'Tautulli is up to date.'} 'message': 'Tautulli is up to date.'
}
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))