Compare commits
74 Commits
v2.0.18-be
...
v2.0.20-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ab34a74210 | ||
![]() |
cfa6de4d91 | ||
![]() |
a5608c7a1e | ||
![]() |
88a7b52e51 | ||
![]() |
e444bad4de | ||
![]() |
5403b0b547 | ||
![]() |
51b5e615f5 | ||
![]() |
700547b63b | ||
![]() |
3f3d1962c7 | ||
![]() |
655a359ef4 | ||
![]() |
90647628c9 | ||
![]() |
681c3ed6e3 | ||
![]() |
7f255943c6 | ||
![]() |
b6e73b5dea | ||
![]() |
eacb7f6ae5 | ||
![]() |
7b300bb87e | ||
![]() |
a81ad27d85 | ||
![]() |
8eed14ff3b | ||
![]() |
82446acdf0 | ||
![]() |
88770b8805 | ||
![]() |
f9f05bbea3 | ||
![]() |
17dd767c22 | ||
![]() |
25b1dc6dd8 | ||
![]() |
b2b1277e37 | ||
![]() |
8e1a588ced | ||
![]() |
9eddfafeae | ||
![]() |
d24a922ccb | ||
![]() |
bbc6482c99 | ||
![]() |
36ff1fb674 | ||
![]() |
f0aa793262 | ||
![]() |
681627a656 | ||
![]() |
87c6ad66fb | ||
![]() |
4ab9eb3bfa | ||
![]() |
2d56ac027b | ||
![]() |
836c4293d6 | ||
![]() |
07092e8aa5 | ||
![]() |
66743c1401 | ||
![]() |
bfe34e060b | ||
![]() |
5ed4236a22 | ||
![]() |
868aeb3902 | ||
![]() |
cbcdac5b04 | ||
![]() |
d473bb3058 | ||
![]() |
066a95d209 | ||
![]() |
c7cc476623 | ||
![]() |
bd44eb7fe4 | ||
![]() |
6ec4f51077 | ||
![]() |
b4a4f60b04 | ||
![]() |
dc4e6edc9a | ||
![]() |
60b362b19e | ||
![]() |
7e81ce8c06 | ||
![]() |
c7f9e2f721 | ||
![]() |
cab8b1c041 | ||
![]() |
16f270691d | ||
![]() |
d94a1efe75 | ||
![]() |
12755970b7 | ||
![]() |
93e4853ea2 | ||
![]() |
5e0c0365fb | ||
![]() |
c2713c53dd | ||
![]() |
90443b4028 | ||
![]() |
e0109ed179 | ||
![]() |
a53afe05a2 | ||
![]() |
a5d2467bfe | ||
![]() |
8447663e27 | ||
![]() |
64d67d8209 | ||
![]() |
78034b82a9 | ||
![]() |
f77bd6c17b | ||
![]() |
2621da7d36 | ||
![]() |
e1dca1509a | ||
![]() |
df016243dd | ||
![]() |
be72693fec | ||
![]() |
33a1ebdb1a | ||
![]() |
030f9d334b | ||
![]() |
dc743ac378 | ||
![]() |
0010cbe21f |
63
API.md
63
API.md
@@ -93,21 +93,6 @@ Returns:
|
|||||||
Delete and recreate the cache directory.
|
Delete and recreate the cache directory.
|
||||||
|
|
||||||
|
|
||||||
### delete_datatable_media_info_cache
|
|
||||||
Delete the media info table cache for a specific library.
|
|
||||||
|
|
||||||
```
|
|
||||||
Required parameters:
|
|
||||||
section_id (str): The id of the Plex library section
|
|
||||||
|
|
||||||
Optional parameters:
|
|
||||||
None
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### delete_image_cache
|
### delete_image_cache
|
||||||
Delete and recreate the image cache directory.
|
Delete and recreate the image cache directory.
|
||||||
|
|
||||||
@@ -176,6 +161,21 @@ Returns:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### delete_media_info_cache
|
||||||
|
Delete the media info table cache for a specific library.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
section_id (str): The id of the Plex library section
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### delete_mobile_device
|
### delete_mobile_device
|
||||||
Remove a mobile device from the database.
|
Remove a mobile device from the database.
|
||||||
|
|
||||||
@@ -863,6 +863,7 @@ Optional parameters:
|
|||||||
start (int): Row to start from, 0
|
start (int): Row to start from, 0
|
||||||
length (int): Number of items to return, 25
|
length (int): Number of items to return, 25
|
||||||
search (str): A string to search for, "Thrones"
|
search (str): A string to search for, "Thrones"
|
||||||
|
refresh (str): "true" to refresh the media info table
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
@@ -1315,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:
|
||||||
@@ -1340,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:
|
||||||
@@ -1365,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:
|
||||||
@@ -1390,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:
|
||||||
@@ -1415,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:
|
||||||
@@ -1440,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:
|
||||||
@@ -1465,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:
|
||||||
@@ -1490,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:
|
||||||
@@ -1515,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:
|
||||||
@@ -1776,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:
|
||||||
@@ -1801,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:
|
||||||
@@ -2408,7 +2420,26 @@ Uninstalls the GeoLite2 database
|
|||||||
|
|
||||||
|
|
||||||
### update
|
### update
|
||||||
Check for Tautulli updates on Github.
|
Update Tautulli.
|
||||||
|
|
||||||
|
|
||||||
|
### update_check
|
||||||
|
Check for Tautulli updates.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json
|
||||||
|
{"result": "success",
|
||||||
|
"update": true,
|
||||||
|
"message": "An update for Tautulli is available."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### update_metadata_details
|
### update_metadata_details
|
||||||
|
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,5 +1,47 @@
|
|||||||
# 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)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Connect to Plex Cloud server without keeping it awake.
|
||||||
|
* Fix: Reconnect to Plex Cloud server after the server wakes up from sleeping.
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Don't send Plex Server Up/Down notifications when Tautulli starts up.
|
||||||
|
* Change: Better handling of Watched notifications.
|
||||||
|
* UI:
|
||||||
|
* New: Added Plex server selection dropdown in the settings.
|
||||||
|
* Fix: Libraries and Users tables not refreshing properly.
|
||||||
|
* Change: Updated the masked info shown to guests.
|
||||||
|
* Change: Check for updates without refreshing to the homepage.
|
||||||
|
* API:
|
||||||
|
* New: Added update_check to the API.
|
||||||
|
* Fix: delete_media_info_cache not deleting the cache.
|
||||||
|
* Change: Document "refresh" parameter for get_library_media_info.
|
||||||
|
* Other:
|
||||||
|
* Fix: Show the full changelog since v2 on a fresh install.
|
||||||
|
|
||||||
|
|
||||||
## v2.0.18-beta (2018-02-12)
|
## v2.0.18-beta (2018-02-12)
|
||||||
|
|
||||||
* Notifications:
|
* Notifications:
|
||||||
|
11
PlexPy.py
11
PlexPy.py
@@ -33,7 +33,7 @@ import signal
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import config, database, logger, web_socket, webstart
|
from plexpy import config, database, logger, webstart
|
||||||
|
|
||||||
|
|
||||||
# Register signals, such as CTRL + C
|
# Register signals, such as CTRL + C
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -194,13 +194,6 @@ def main():
|
|||||||
# Start the background threads
|
# Start the background threads
|
||||||
plexpy.start()
|
plexpy.start()
|
||||||
|
|
||||||
# Open connection for websocket
|
|
||||||
try:
|
|
||||||
web_socket.start_thread()
|
|
||||||
except:
|
|
||||||
logger.warn(u"Websocket :: Unable to open connection.")
|
|
||||||
plexpy.initialize_scheduler()
|
|
||||||
|
|
||||||
# Force the http port if neccessary
|
# Force the http port if neccessary
|
||||||
if args.port:
|
if args.port:
|
||||||
http_port = args.port
|
http_port = args.port
|
||||||
|
@@ -44,16 +44,24 @@
|
|||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
||||||
<div id="updatebar" style="display: none;">
|
<div id="updatebar" style="display: none;">
|
||||||
You're 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">Close</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're ${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">Close</a>
|
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||||
</div>
|
</div>
|
||||||
|
% else:
|
||||||
|
<div id="updatebar" style="display: none;"></div>
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
<nav class="navbar navbar-fixed-top">
|
<nav class="navbar navbar-fixed-top">
|
||||||
@@ -66,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">
|
||||||
@@ -289,14 +297,48 @@ ${next.modalIncludes()}
|
|||||||
% endif
|
% endif
|
||||||
<script>
|
<script>
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('#updateDismiss').click(function() {
|
$('body').on('click', '#updateDismiss', function() {
|
||||||
$('#updatebar').slideUp('slow');
|
$('#updatebar').fadeOut();
|
||||||
// Set cookie to remember dismiss decision for 1 hour.
|
// Set cookie to remember dismiss decision for 1 hour.
|
||||||
setCookie('updateDismiss', 'true', 1/24);
|
setCookie('updateDismiss', 'true', 1/24);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!getCookie('updateDismiss')) {
|
if (!getCookie('updateDismiss')) {
|
||||||
$('#updatebar').show();
|
if ($('#updatebar').html().length > 0) {
|
||||||
|
$('#updatebar').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUpdate(_callback) {
|
||||||
|
// Allow the update bar to show again if previously dismissed.
|
||||||
|
setCookie('updateDismiss', 'true', 0);
|
||||||
|
$.ajax({
|
||||||
|
url: 'update_check',
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
var result = $.parseJSON(xhr.responseText);
|
||||||
|
var msg = '';
|
||||||
|
if (result.update === 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();
|
||||||
|
} 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>';
|
||||||
|
$('#updatebar').html(msg).fadeIn();
|
||||||
|
} else if (result.update === false) {
|
||||||
|
showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_callback) {
|
||||||
|
_callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#nav-shutdown").click(function() {
|
$("#nav-shutdown").click(function() {
|
||||||
@@ -315,11 +357,9 @@ ${next.modalIncludes()}
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#nav-update").first().one("click", function () {
|
$('#nav-update').click(function () {
|
||||||
// Allow the update bar to show again if previously dismissed.
|
$(this).html('<i class="fa fa-fw fa-spin fa-refresh"></i> Checking');
|
||||||
setCookie('updateDismiss', 'true', 0);
|
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-circle-up"></i> Check for Updates'); });
|
||||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking');
|
|
||||||
window.location.href = "checkGithub";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
|
@@ -60,7 +60,8 @@ select[multiple] option {
|
|||||||
-moz-border-radius: 2px;
|
-moz-border-radius: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
select.form-control {
|
select.form-control,
|
||||||
|
div.form-control .selectize-input {
|
||||||
margin: 5px 0 5px 0;
|
margin: 5px 0 5px 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: 0px solid #444;
|
border: 0px solid #444;
|
||||||
@@ -80,12 +81,37 @@ select.form-control {
|
|||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
.selectize-control.form-control .selectize-input {
|
.selectize-control.form-control .selectize-input {
|
||||||
display: flex;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
.selectize-control.form-control.selectize-pms-ip .selectize-input {
|
||||||
|
padding-left: 12px !important;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
min-height: 32px !important;
|
||||||
|
}
|
||||||
|
.input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
|
||||||
|
max-width: 450px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
|
||||||
|
max-width: 360px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#selectize-pms-ip-container .selectize-dropdown.form-control.selectize-pms-ip {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-dropdown .selectize-dropdown-content {
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
.wizard-input-section .selectize-dropdown.form-control.selectize-pms-ip {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
@@ -134,33 +160,40 @@ select.form-control:focus,
|
|||||||
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
||||||
fill: #999 !important;
|
fill: #999 !important;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-value {
|
.selectize-input > div .item-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.selectize-input > div .item-value {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-text + .item-value {
|
.selectize-input > div .item-text + .item-value {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-value:before {
|
.selectize-input > div .item-value:before {
|
||||||
content: '<';
|
content: '<';
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .item-value:after {
|
.selectize-input > div .item-value:after {
|
||||||
content: '>';
|
content: '>';
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .caption {
|
.selectize-dropdown .caption {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: block;
|
display: block;
|
||||||
color: #a0a0a0;
|
color: #a0a0a0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .select-all,
|
.selectize-dropdown .select-all,
|
||||||
.selectize-control .selectize-dropdown .remove-all {
|
.selectize-dropdown .remove-all {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .border-all {
|
.selectize-dropdown .border-all {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: block;
|
display: block;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -169,7 +202,7 @@ select.form-control:focus,
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #e5e5e5;
|
background-color: #e5e5e5;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-dropdown .border-all:last-child {
|
.selectize-dropdown .border-all:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.selectize-dropdown .optgroup-header {
|
.selectize-dropdown .optgroup-header {
|
||||||
@@ -619,18 +652,8 @@ textarea.form-control:focus {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.form-control-feedback {
|
.form-control-feedback {
|
||||||
position: absolute;
|
|
||||||
color: #F9AA03;
|
color: #F9AA03;
|
||||||
top: 0;
|
margin: 5px 40px 5px 0;
|
||||||
right: 0;
|
|
||||||
margin: 5px 10px 5px 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: block;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
text-align: center;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
.form-control[readonly] {
|
.form-control[readonly] {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
@@ -2372,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,
|
||||||
@@ -3123,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;
|
||||||
}
|
}
|
||||||
@@ -3212,16 +3238,16 @@ div.dataTables_info {
|
|||||||
}
|
}
|
||||||
#updatebar {
|
#updatebar {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
|
opacity: 0.95;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
display: none;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
padding: 7px 10px;
|
padding: 10px 10px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
width: 250px;
|
width: 400px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -3293,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;
|
||||||
|
@@ -67,8 +67,15 @@ DOCUMENTATION :: END
|
|||||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||||
import plexpy
|
import plexpy
|
||||||
%>
|
%>
|
||||||
<% data = defaultdict(lambda: 'Unknown', **session) %>
|
<%
|
||||||
<% sk = data['session_key'] %>
|
data = defaultdict(lambda: 'Unknown', **session)
|
||||||
|
sk = data['session_key']
|
||||||
|
|
||||||
|
href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#'
|
||||||
|
parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#'
|
||||||
|
grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#'
|
||||||
|
user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#'
|
||||||
|
%>
|
||||||
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
|
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
|
||||||
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
|
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
|
||||||
<div class="dashboard-activity-container">
|
<div class="dashboard-activity-container">
|
||||||
@@ -89,15 +96,15 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
% if data['channel_stream'] == 0:
|
% if data['channel_stream'] == 0:
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<a id="poster-url-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">
|
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
|
||||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<a id="poster-url-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">
|
<a id="poster-url-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">
|
||||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<a id="poster-url-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}">
|
<a id="poster-url-${sk}" href="${parent_href}" title="${data['parent_title']}">
|
||||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif data['media_type'] in ('photo', 'clip'):
|
% elif data['media_type'] in ('photo', 'clip'):
|
||||||
@@ -269,8 +276,9 @@ DOCUMENTATION :: END
|
|||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Location</div>
|
<div class="sub-heading">Location</div>
|
||||||
<div class="sub-value time-right">
|
<div class="sub-value time-right">
|
||||||
|
<span id="location-${sk}">${data['location'].upper()}</span>:
|
||||||
% if data['ip_address'] != 'N/A':
|
% if data['ip_address'] != 'N/A':
|
||||||
<span id="location-${sk}">${data['location'].upper()}</span>: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||||
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
||||||
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
||||||
</a>
|
</a>
|
||||||
@@ -352,13 +360,9 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-wrapper">
|
<div class="dashboard-activity-metadata-wrapper">
|
||||||
% if data['user_id']:
|
<a href="${user_href}" title="${data['friendly_name']}">
|
||||||
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">
|
|
||||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
||||||
</a>
|
</a>
|
||||||
% else:
|
|
||||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
|
||||||
% endif
|
|
||||||
<div class="dashboard-activity-metadata-title-container">
|
<div class="dashboard-activity-metadata-title-container">
|
||||||
<div id="play-state-${sk}" class="dashboard-activity-metadata-play_state-icon" title="${data['state'].capitalize()}">
|
<div id="play-state-${sk}" class="dashboard-activity-metadata-play_state-icon" title="${data['state'].capitalize()}">
|
||||||
% if data['state'] == 'playing':
|
% if data['state'] == 'playing':
|
||||||
@@ -371,21 +375,21 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-title">
|
<div class="dashboard-activity-metadata-title">
|
||||||
% if data['channel_stream'] == 0:
|
% if data['channel_stream'] == 0:
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
<a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<a href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
<a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||||
- <a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<a id="metadata-grandparent_title-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
<a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||||
- <a id="metadata-title-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
- <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
|
||||||
% elif data['media_type'] == 'photo':
|
% elif data['media_type'] == 'photo':
|
||||||
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
||||||
% elif data['media_type'] == 'clip':
|
% elif data['media_type'] == 'clip':
|
||||||
<span title="${data['title']}">${data['title']}</span>
|
<span title="${data['title']}">${data['title']}</span>
|
||||||
% else:
|
% else:
|
||||||
<span title="${data['title']}">${data['title']}</span>
|
<span title="${data['title']}">${data['title']}</span>
|
||||||
% endif
|
% endif
|
||||||
% elif data['media_type'] == 'episode' and data['grandparent_title']:
|
% elif data['media_type'] == 'episode' and data['grandparent_title']:
|
||||||
<span title="${data['grandparent_title']}">${data['grandparent_title']}</span>
|
<span title="${data['grandparent_title']}">${data['grandparent_title']}</span>
|
||||||
- <span title="${data['title']}">${data['title']}</span>
|
- <span title="${data['title']}">${data['title']}</span>
|
||||||
@@ -425,10 +429,10 @@ DOCUMENTATION :: END
|
|||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||||
% elif data['media_type'] == 'episode':
|
% elif data['media_type'] == 'episode':
|
||||||
<a href="info?rating_key=${data['parent_rating_key']}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
<a href="${parent_href}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
||||||
· <a href="info?rating_key=${data['rating_key']}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
· <a href="${href}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
||||||
% elif data['media_type'] == 'track':
|
% elif data['media_type'] == 'track':
|
||||||
<a id="metadata-parent_title-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
|
<a id="metadata-parent_title-${sk}" href="${parent_href}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
|
||||||
% elif data['media_type'] == 'photo':
|
% elif data['media_type'] == 'photo':
|
||||||
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
|
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
|
||||||
% else:
|
% else:
|
||||||
@@ -453,11 +457,7 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-user">
|
<div class="dashboard-activity-metadata-user">
|
||||||
% if data['user_id']:
|
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
||||||
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
|
||||||
% else:
|
|
||||||
${data['friendly_name']}
|
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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;">
|
||||||
|
@@ -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:
|
||||||
|
@@ -88,17 +88,19 @@ 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
|
||||||
<a id="stats-thumb-url-${stat_id}" href="info?rating_key=${row0['rating_key']}" title="${row0['title']}">
|
<% height, type = ('300', 'cover') if stat_id in ('top_music', 'popular_music') else ('450', 'poster') %>
|
||||||
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else 'poster' %>
|
<% 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']}">
|
||||||
% 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
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
% elif stat_id == 'top_users':
|
% elif stat_id == 'top_users':
|
||||||
<a id="stats-thumb-url-${stat_id}" href="user?user_id=${row0['user_id']}" title="${row0['friendly_name']}" class="hidden-xs">
|
<% user_href = 'user?user_id={}'.format(row0['user_id']) if row0['user_id'] else '#' %>
|
||||||
|
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
|
||||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
||||||
</a>
|
</a>
|
||||||
% elif stat_id == 'top_platforms':
|
% elif stat_id == 'top_platforms':
|
||||||
@@ -127,26 +129,20 @@ DOCUMENTATION :: END
|
|||||||
% for row in top_stat['rows']:
|
% for row in top_stat['rows']:
|
||||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
|
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
|
||||||
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
||||||
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}"
|
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
|
||||||
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
|
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
|
||||||
<div class="sub-list">${loop.index + 1}</div>
|
<div class="sub-list">${loop.index + 1}</div>
|
||||||
<div class="sub-value">
|
<div class="sub-value">
|
||||||
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
||||||
% if top_stat['rows'][loop.index]['rating_key']:
|
<% href = 'info?rating_key={}'.format(row['rating_key']) if row['rating_key'] else '#' %>
|
||||||
<a href="info?rating_key=${row['rating_key']}" title="${row['title']}">
|
<a href="${href}" title="${row['title']}">
|
||||||
${row['title']}
|
${row['title']}
|
||||||
</a>
|
</a>
|
||||||
% else:
|
|
||||||
${row['title']}
|
|
||||||
% endif
|
|
||||||
% elif stat_id == 'top_users':
|
% elif stat_id == 'top_users':
|
||||||
% if top_stat['rows'][loop.index]['user_id']:
|
<% user_href = 'user?user_id={}'.format(row['user_id']) if row['user_id'] else '#' %>
|
||||||
<a href="user?user_id=${row['user_id']}" title="${row['friendly_name']}">
|
<a href="${user_href}" title="${row['friendly_name']}">
|
||||||
${row['friendly_name']}
|
${row['friendly_name']}
|
||||||
</a>
|
</a>
|
||||||
% else:
|
|
||||||
${row['friendly_name']}
|
|
||||||
% endif
|
|
||||||
% elif stat_id == 'top_platforms':
|
% elif stat_id == 'top_platforms':
|
||||||
${row['platform']}
|
${row['platform']}
|
||||||
% elif stat_id == 'most_concurrent':
|
% elif stat_id == 'most_concurrent':
|
||||||
@@ -182,13 +178,22 @@ DOCUMENTATION :: END
|
|||||||
var stat_id = $(elem).data('stat_id');
|
var stat_id = $(elem).data('stat_id');
|
||||||
var art = $(elem).data('art');
|
var art = $(elem).data('art');
|
||||||
var thumb = $(elem).data('thumb');
|
var thumb = $(elem).data('thumb');
|
||||||
|
var user_id = $(elem).data('user_id');
|
||||||
|
var user_thumb = $(elem).data('user_thumb');
|
||||||
|
var rating_key = $(elem).data('rating_key');
|
||||||
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
|
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
|
||||||
|
var href;
|
||||||
|
|
||||||
if (stat_id == 'most_concurrent') {
|
if (stat_id == 'most_concurrent') {
|
||||||
return
|
return
|
||||||
} else if (stat_id == 'top_users') {
|
} else if (stat_id == 'top_users') {
|
||||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (thumb || 'images/gravatar-default.png') + ')');
|
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
|
||||||
$('#stats-thumb-url-' + stat_id).attr('href', 'user?user_id=' + $(elem).data('user_id')).prop('title', $(elem).data('friendly_name'));
|
if (user_id) {
|
||||||
|
href = 'user?user_id=' + user_id;
|
||||||
|
} else {
|
||||||
|
href = '#';
|
||||||
|
}
|
||||||
|
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
|
||||||
} else if (stat_id == 'top_platforms') {
|
} else if (stat_id == 'top_platforms') {
|
||||||
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
||||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||||
@@ -197,7 +202,12 @@ DOCUMENTATION :: END
|
|||||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||||
}).addClass('platform-' + $(elem).data('platform'));
|
}).addClass('platform-' + $(elem).data('platform'));
|
||||||
} else {
|
} else {
|
||||||
$('#stats-thumb-url-' + stat_id).attr('href', 'info?rating_key=' + $(elem).data('rating_key')).prop('title', $(elem).data('title'));
|
if (rating_key) {
|
||||||
|
href = 'info?rating_key=' + rating_key;
|
||||||
|
} else {
|
||||||
|
href = '#';
|
||||||
|
}
|
||||||
|
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
|
||||||
if (art) {
|
if (art) {
|
||||||
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&fallback=art)');
|
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&fallback=art)');
|
||||||
} else {
|
} else {
|
||||||
@@ -207,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)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
data/interfaces/default/images/logo-tautulli-100.png
Normal file
BIN
data/interfaces/default/images/logo-tautulli-100.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
BIN
data/interfaces/default/images/logo-tautulli-45.png
Normal file
BIN
data/interfaces/default/images/logo-tautulli-45.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
@@ -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>
|
||||||
|
@@ -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">
|
||||||
|
@@ -26,7 +26,7 @@ function refreshTab() {
|
|||||||
|
|
||||||
function showMsg(msg, loader, timeout, ms, error) {
|
function showMsg(msg, loader, timeout, ms, error) {
|
||||||
var feedback = $("#ajaxMsg");
|
var feedback = $("#ajaxMsg");
|
||||||
update = $("#updatebar");
|
var update = $("#updatebar");
|
||||||
if (update.is(":visible")) {
|
if (update.is(":visible")) {
|
||||||
var height = update.height() + 35;
|
var height = update.height() + 35;
|
||||||
feedback.css("bottom", height + "px");
|
feedback.css("bottom", height + "px");
|
||||||
@@ -35,7 +35,7 @@ function showMsg(msg, loader, timeout, ms, error) {
|
|||||||
}
|
}
|
||||||
var message = $("<div class='msg'>" + msg + "</div>");
|
var message = $("<div class='msg'>" + msg + "</div>");
|
||||||
if (loader) {
|
if (loader) {
|
||||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||||
feedback.css("padding", "14px 10px")
|
feedback.css("padding", "14px 10px")
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@@ -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> Fetching rows...";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching 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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -113,7 +113,7 @@ login_log_table_options = {
|
|||||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching 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');
|
||||||
|
@@ -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> Fetching rows...";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching 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();
|
||||||
|
@@ -180,18 +180,20 @@
|
|||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$("#refresh-libraries-list").click(function () {
|
$("#refresh-libraries-list").click(function () {
|
||||||
|
showMsg('Refreshing libraries list...', true, false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'refresh_libraries_list',
|
url: 'refresh_libraries_list',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success: function (data) {
|
complete: function (xhr, status) {
|
||||||
showMsg('<i class="fa fa-refresh"></i> Libraries list refresh started...', false, true, 2000, false);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
},
|
var msg = result.message;
|
||||||
complete: function (data) {
|
if (result.result == 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> Libraries list refreshed.', false, true, 2000, false);
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||||
},
|
libraries_list_table.draw();
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
} else {
|
||||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh libraries list.', false, true, 2000, true);
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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({
|
||||||
|
@@ -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">
|
||||||
|
@@ -518,7 +518,6 @@
|
|||||||
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)';
|
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)';
|
||||||
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
|
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
|
||||||
plugins: ['remove_button'],
|
plugins: ['remove_button'],
|
||||||
persist: false,
|
|
||||||
maxItems: null,
|
maxItems: null,
|
||||||
render: {
|
render: {
|
||||||
item: function(item, escape) {
|
item: function(item, escape) {
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import common, notifiers
|
from plexpy import common, notifiers
|
||||||
from plexpy.helpers import anon_url
|
from plexpy.helpers import anon_url, checked
|
||||||
|
|
||||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'])
|
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'])
|
||||||
%>
|
%>
|
||||||
@@ -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>
|
||||||
@@ -623,9 +623,11 @@
|
|||||||
<div class="form-group has-feedback" id="pms_ip_group">
|
<div class="form-group has-feedback" id="pms_ip_group">
|
||||||
<label for="pms_ip">Plex IP or Hostname</label>
|
<label for="pms_ip">Plex IP or Hostname</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-9" id="selectize-pms-ip-container">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="pms-settings form-control" id="pms_ip" name="pms_ip" value="${config['pms_ip']}" size="30" data-parsley-trigger="change" aria-describedby="server-verified" data-parsley-errors-container="#pms_ip_error" required>
|
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip" name="pms_ip" data-parsley-trigger="change" aria-describedby="server-verified" data-parsley-errors-container="#pms_ip_error" required>
|
||||||
|
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
|
||||||
|
</select>
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button>
|
<button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -634,7 +636,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
|
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">IP Address or hostname for Plex Media Server.</p>
|
<p class="help-block">Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pms_port">Plex Port</label>
|
<label for="pms_port">Plex Port</label>
|
||||||
@@ -648,27 +650,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1" ${config['pms_is_remote']}> Remote Server
|
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${checked(config['pms_is_remote'])}> Remote Server
|
||||||
|
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Check this if your Plex Server is not on the same local network as Tautulli.</p>
|
<p class="help-block">Check this if your Plex Server is not on the same local network as Tautulli.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Use SSL
|
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${checked(config['pms_ssl'])}> Use SSL
|
||||||
|
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
|
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox advanced-setting">
|
<div class="checkbox advanced-setting">
|
||||||
% if config['pms_is_cloud']:
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" disabled> Manual Connection
|
|
||||||
</label>
|
|
||||||
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
|
||||||
% else:
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
|
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
|
||||||
</label>
|
</label>
|
||||||
% endif
|
<span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||||
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
|
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group advanced-setting">
|
<div class="form-group advanced-setting">
|
||||||
@@ -689,6 +687,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
|
||||||
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
|
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
|
||||||
|
|
||||||
@@ -721,16 +720,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
% if config['pms_is_cloud']:
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" disabled> Monitor Plex Updates
|
|
||||||
</label>
|
|
||||||
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
|
||||||
% else:
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
|
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
|
||||||
</label>
|
</label>
|
||||||
% endif
|
<span id="cloudMonitorUpdates" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||||
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
|
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="pms_update_options">
|
<div id="pms_update_options">
|
||||||
@@ -753,17 +746,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
% if config['pms_is_cloud']:
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" disabled> Monitor Plex Remote Access
|
|
||||||
</label>
|
|
||||||
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
|
||||||
% else:
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
|
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
|
||||||
</label>
|
</label>
|
||||||
|
<span id="cloudMonitorRemoteAccess" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||||
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
||||||
% endif
|
|
||||||
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
|
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1601,7 +1588,7 @@ $(document).ready(function() {
|
|||||||
if (serverChanged || authChanged || httpChanged || directoryChanged) {
|
if (serverChanged || authChanged || httpChanged || directoryChanged) {
|
||||||
$('#restart-modal').modal('show');
|
$('#restart-modal').modal('show');
|
||||||
}
|
}
|
||||||
$("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0)
|
$("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0);
|
||||||
getConfigurationTable();
|
getConfigurationTable();
|
||||||
getSchedulerTable();
|
getSchedulerTable();
|
||||||
getNotifiersTable();
|
getNotifiersTable();
|
||||||
@@ -1661,11 +1648,11 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#menu_link_update_check').click(function() {
|
$('#menu_link_update_check').click(function() {
|
||||||
// Allow the update bar to show again if previously dismissed.
|
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking').prop('disabled', true);
|
||||||
setCookie('updateDismiss', 'true', 0);
|
checkUpdate(function () {
|
||||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking');
|
$('#menu_link_update_check').html('<i class="fa fa-arrow-circle-up"></i> Check for Updates')
|
||||||
$(this).prop('disabled', true);
|
.prop('disabled', false);
|
||||||
window.location.href = 'checkGithub';
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#modal_link_restart').click(function() {
|
$('#modal_link_restart').click(function() {
|
||||||
@@ -1787,12 +1774,138 @@ $(document).ready(function() {
|
|||||||
verifyServer();
|
verifyServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.checkbox-toggle').click(function () {
|
||||||
|
var configToggle = $(this).data('id');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$('#'+configToggle).val(1);
|
||||||
|
} else {
|
||||||
|
$('#'+configToggle).val(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var $select_pms = $('#pms_ip').selectize({
|
||||||
|
createOnBlur: true,
|
||||||
|
openOnFocus: true,
|
||||||
|
maxItems: 1,
|
||||||
|
closeAfterSelect: true,
|
||||||
|
sortField: 'label',
|
||||||
|
searchField: ['label', 'value'],
|
||||||
|
inputClass: 'form-control selectize-input',
|
||||||
|
dropdownParent: '#selectize-pms-ip-container',
|
||||||
|
render: {
|
||||||
|
item: function (item, escape) {
|
||||||
|
var label = item.label || item.value;
|
||||||
|
var caption = item.label ? item.value : null;
|
||||||
|
return '<div data-ssl="' + item.httpsRequired +
|
||||||
|
'" data-local="' + item.local +
|
||||||
|
'" data-identifier="' + item.clientIdentifier +
|
||||||
|
'" data-ip="' + item.ip +
|
||||||
|
'" data-port="' + item.port +
|
||||||
|
'" data-is_cloud="' + item.is_cloud +
|
||||||
|
'" data-label="' + item.label + '">' +
|
||||||
|
'<span class="item-text">' + escape(label) + '</span>' +
|
||||||
|
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
},
|
||||||
|
option: function (item, escape) {
|
||||||
|
var label = item.label || item.value;
|
||||||
|
var caption = item.label ? item.value : null;
|
||||||
|
return '<div data-ssl="' + item.httpsRequired +
|
||||||
|
'" data-local="' + item.local +
|
||||||
|
'" data-identifier="' + item.clientIdentifier +
|
||||||
|
'" data-ip="' + item.ip +
|
||||||
|
'" data-port="' + item.port +
|
||||||
|
'" data-is_cloud="' + item.is_cloud +
|
||||||
|
'" data-label="' + item.label + '">' +
|
||||||
|
escape(label) +
|
||||||
|
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: function(input) {
|
||||||
|
return {label: '', value: input};
|
||||||
|
},
|
||||||
|
onChange: function (item) {
|
||||||
|
var pms_ip_selected = this.getItem(item)[0];
|
||||||
|
var identifier = $(pms_ip_selected).data('identifier');
|
||||||
|
var port = $(pms_ip_selected).data('port');
|
||||||
|
var local = $(pms_ip_selected).data('local');
|
||||||
|
var ssl = $(pms_ip_selected).data('ssl');
|
||||||
|
var is_cloud = $(pms_ip_selected).data('is_cloud');
|
||||||
|
|
||||||
|
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||||
|
$('#pms_port').val(port !== 'undefined' ? port : 32400);
|
||||||
|
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||||
|
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||||
|
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||||
|
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||||
|
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
|
||||||
|
$('#pms_url_manual').prop('checked', false);
|
||||||
|
PMSCloudCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var select_pms = $select_pms[0].selectize;
|
||||||
|
|
||||||
|
function getServerOptions(token) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'discover',
|
||||||
|
data: {
|
||||||
|
token: token
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result) {
|
||||||
|
var existing_value = $('#pms_ip').val();
|
||||||
|
result.forEach(function (item) {
|
||||||
|
if (item.value === existing_value) {
|
||||||
|
select_pms.updateOption(item.value, item);
|
||||||
|
} else {
|
||||||
|
select_pms.addOption(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getServerOptions();
|
||||||
|
|
||||||
|
function PMSCloudCheck() {
|
||||||
|
if ($('#pms_is_cloud').val() === "1") {
|
||||||
|
$('#pms_port').val(443).prop('readonly', true);
|
||||||
|
$('#pms_is_remote_checkbox').prop('checked', true).prop('disabled', true);
|
||||||
|
$('#pms_is_remote').val(1);
|
||||||
|
$('#pms_ssl_checkbox').prop('checked', true).prop('disabled', true);
|
||||||
|
$('#pms_ssl').val(1);
|
||||||
|
$('#pms_url_manual').prop('checked', false).prop('disabled', true);
|
||||||
|
$('#monitor_pms_updates').prop('checked', false).prop('disabled', true);
|
||||||
|
$('#pms_update_options').hide();
|
||||||
|
$('#monitor_remote_access').prop('checked', false).prop('disabled', true);
|
||||||
|
$('#cloudManualConnection').show();
|
||||||
|
$('#cloudMonitorUpdates').show();
|
||||||
|
$('#cloudMonitorRemoteAccess').show();
|
||||||
|
$('#remoteAccessCheck').hide();
|
||||||
|
} else {
|
||||||
|
$('#pms_port').prop('readonly', false);
|
||||||
|
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||||
|
$('#pms_is_remote').val($('#pms_is_remote_checkbox').is(':checked') ? 1 : 0);
|
||||||
|
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||||
|
$('#pms_ssl').val($('#pms_ssl_checkbox').is(':checked') ? 1 : 0);
|
||||||
|
$('#pms_url_manual').prop('disabled', false);
|
||||||
|
$('#monitor_pms_updates').prop('disabled', false);
|
||||||
|
$('#monitor_remote_access').prop('disabled', false);
|
||||||
|
$('#cloudManualConnection').hide();
|
||||||
|
$('#cloudMonitorUpdates').hide();
|
||||||
|
$('#cloudMonitorRemoteAccess').hide();
|
||||||
|
remoteAccessEnabledCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PMSCloudCheck();
|
||||||
|
|
||||||
function verifyServer(_callback) {
|
function verifyServer(_callback) {
|
||||||
var pms_ip = $("#pms_ip").val();
|
var pms_ip = $("#pms_ip").val();
|
||||||
var pms_port = $("#pms_port").val();
|
var pms_port = $("#pms_port").val();
|
||||||
var pms_identifier = $("#pms_identifier").val();
|
var pms_identifier = $("#pms_identifier").val();
|
||||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
var pms_ssl = $("#pms_ssl").val();
|
||||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
var pms_is_remote = $("#pms_is_remote").val();
|
||||||
|
|
||||||
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
||||||
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||||
@@ -1875,6 +1988,7 @@ $(document).ready(function() {
|
|||||||
$("#pms-token-status").html('<i class="fa fa-check"></i> ' + msg);
|
$("#pms-token-status").html('<i class="fa fa-check"></i> ' + msg);
|
||||||
$("#pms_token").val(authToken);
|
$("#pms_token").val(authToken);
|
||||||
$('#pms-auth-modal').modal('hide');
|
$('#pms-auth-modal').modal('hide');
|
||||||
|
getServerOptions(authToken);
|
||||||
} else {
|
} else {
|
||||||
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> ' + msg);
|
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> ' + msg);
|
||||||
}
|
}
|
||||||
@@ -1903,18 +2017,20 @@ $(document).ready(function() {
|
|||||||
pms_logs_debug = false;
|
pms_logs_debug = false;
|
||||||
pms_logs = false;
|
pms_logs = false;
|
||||||
|
|
||||||
$.ajax({
|
function remoteAccessEnabledCheck() {
|
||||||
url: 'get_server_pref',
|
$.ajax({
|
||||||
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
url: 'get_server_pref',
|
||||||
async: true,
|
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||||
success: function(data) {
|
async: true,
|
||||||
if (data !== 'true') {
|
success: function(data) {
|
||||||
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
if (data !== 'true') {
|
||||||
$("#monitor_remote_access").attr("disabled", true);
|
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||||
$("#monitor_remote_access").attr("checked", false);
|
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
remoteAccessEnabledCheck();
|
||||||
|
|
||||||
// Sortable home_sections
|
// Sortable home_sections
|
||||||
function set_home_sections() {
|
function set_home_sections() {
|
||||||
@@ -1924,11 +2040,11 @@ $(document).ready(function() {
|
|||||||
home_sections.push(sec.value);
|
home_sections.push(sec.value);
|
||||||
});
|
});
|
||||||
$('#home_sections').val(home_sections);
|
$('#home_sections').val(home_sections);
|
||||||
};
|
}
|
||||||
|
|
||||||
var sec_cards = ${config['home_sections'] | n};
|
var sec_cards = ${config['home_sections'] | n};
|
||||||
sec_cards.reverse().forEach(function (item) {
|
sec_cards.reverse().forEach(function (item) {
|
||||||
$('#hsec-' + item).prop('checked', !$(this).prop('checked'))
|
$('#hsec-' + item).prop('checked', !$(this).prop('checked'));
|
||||||
$('#hsec-' + item).closest('li.card').prependTo('#sortable_home_sections');
|
$('#hsec-' + item).closest('li.card').prependTo('#sortable_home_sections');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1940,7 +2056,7 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('[id^=hsec-]').change(function() { set_home_sections(); });
|
$('[id^=hsec-]').change(function() { set_home_sections(); });
|
||||||
set_home_sections()
|
set_home_sections();
|
||||||
|
|
||||||
// Sortable home_stats_cards
|
// Sortable home_stats_cards
|
||||||
function set_home_stats_cards() {
|
function set_home_stats_cards() {
|
||||||
@@ -1950,11 +2066,11 @@ $(document).ready(function() {
|
|||||||
home_stats_cards.push(card.value);
|
home_stats_cards.push(card.value);
|
||||||
});
|
});
|
||||||
$('#home_stats_cards').val(home_stats_cards);
|
$('#home_stats_cards').val(home_stats_cards);
|
||||||
};
|
}
|
||||||
|
|
||||||
var config_cards = ${config['home_stats_cards'] | n};
|
var config_cards = ${config['home_stats_cards'] | n};
|
||||||
config_cards.reverse().forEach(function (item) {
|
config_cards.reverse().forEach(function (item) {
|
||||||
$('#hscard-' + item).prop('checked', !$(this).prop('checked'))
|
$('#hscard-' + item).prop('checked', !$(this).prop('checked'));
|
||||||
$('#hscard-' + item).closest('li.card').prependTo('#sortable_home_stats_cards');
|
$('#hscard-' + item).closest('li.card').prependTo('#sortable_home_stats_cards');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1966,7 +2082,7 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('[id^=hscard-]').change(function() { set_home_stats_cards(); });
|
$('[id^=hscard-]').change(function() { set_home_stats_cards(); });
|
||||||
set_home_stats_cards()
|
set_home_stats_cards();
|
||||||
|
|
||||||
// Sortable home_library_cards
|
// Sortable home_library_cards
|
||||||
function set_home_library_cards() {
|
function set_home_library_cards() {
|
||||||
@@ -1976,7 +2092,7 @@ $(document).ready(function() {
|
|||||||
home_library_cards.push(card.value);
|
home_library_cards.push(card.value);
|
||||||
});
|
});
|
||||||
$('#home_library_cards').val(home_library_cards);
|
$('#home_library_cards').val(home_library_cards);
|
||||||
};
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_library_sections',
|
url: 'get_library_sections',
|
||||||
@@ -2015,12 +2131,10 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
function allowPlexAdminCheck () {
|
function allowPlexAdminCheck () {
|
||||||
if ($("#http_basic_auth").is(":checked")) {
|
if ($("#http_basic_auth").is(":checked")) {
|
||||||
$("#http_plex_admin").attr("disabled", true);
|
$("#http_plex_admin").attr("checked", false).attr("disabled", true);
|
||||||
$("#http_plex_admin").attr("checked", false);
|
|
||||||
$("#allowPlexCheck").html("Plex admin login cannot be enabled with basic authentication.");
|
$("#allowPlexCheck").html("Plex admin login cannot be enabled with basic authentication.");
|
||||||
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
|
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
|
||||||
$("#http_plex_admin").attr("disabled", true);
|
$("#http_plex_admin").attr("checked", false).attr("disabled", true);
|
||||||
$("#http_plex_admin").attr("checked", false);
|
|
||||||
$("#allowPlexCheck").html("You must set an admin username and password above to allow Plex admin login.");
|
$("#allowPlexCheck").html("You must set an admin username and password above to allow Plex admin login.");
|
||||||
} else {
|
} else {
|
||||||
$("#http_plex_admin").attr("disabled", false);
|
$("#http_plex_admin").attr("disabled", false);
|
||||||
@@ -2035,12 +2149,10 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
function allowGuestAccessCheck () {
|
function allowGuestAccessCheck () {
|
||||||
if ($("#http_basic_auth").is(":checked")) {
|
if ($("#http_basic_auth").is(":checked")) {
|
||||||
$("#allow_guest_access").attr("disabled", true);
|
$("#allow_guest_access").attr("checked", false).attr("disabled", true);
|
||||||
$("#allow_guest_access").attr("checked", false);
|
|
||||||
$("#allowGuestCheck").html("Guest access cannot be enabled with basic authentication.");
|
$("#allowGuestCheck").html("Guest access cannot be enabled with basic authentication.");
|
||||||
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
|
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
|
||||||
$("#allow_guest_access").attr("disabled", true);
|
$("#allow_guest_access").attr("checked", false).attr("disabled", true);
|
||||||
$("#allow_guest_access").attr("checked", false);
|
|
||||||
$("#allowGuestCheck").html("You must set an admin username and password above to allow guest access.");
|
$("#allowGuestCheck").html("You must set an admin username and password above to allow guest access.");
|
||||||
} else {
|
} else {
|
||||||
$("#allow_guest_access").attr("disabled", false);
|
$("#allow_guest_access").attr("disabled", false);
|
||||||
@@ -2055,8 +2167,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
function hashPasswordCheck () {
|
function hashPasswordCheck () {
|
||||||
if ($("#http_basic_auth").is(":checked")) {
|
if ($("#http_basic_auth").is(":checked")) {
|
||||||
$("#http_hash_password").attr("checked", false);
|
$("#http_hash_password").attr("checked", false).attr("disabled", true);
|
||||||
$("#http_hash_password").attr("disabled", true);
|
|
||||||
$("#hashPasswordCheck").html("Password cannot be hashed with basic authentication.");
|
$("#hashPasswordCheck").html("Password cannot be hashed with basic authentication.");
|
||||||
} else {
|
} else {
|
||||||
$("#http_hash_password").attr("disabled", false);
|
$("#http_hash_password").attr("disabled", false);
|
||||||
|
@@ -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() {
|
||||||
|
@@ -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> Select 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> Select 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> 
|
</button> 
|
||||||
</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>
|
||||||
|
@@ -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> Select 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> 
|
||||||
|
</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({
|
||||||
|
@@ -184,18 +184,20 @@
|
|||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$("#refresh-users-list").click(function() {
|
$("#refresh-users-list").click(function() {
|
||||||
|
showMsg('Refreshing users list...', true, false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'refresh_users_list',
|
url: 'refresh_users_list',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success: function(data) {
|
complete: function (xhr, status) {
|
||||||
showMsg('<i class="fa fa-check"></i> Users list refresh started...', false, true, 2000, false);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
},
|
var msg = result.message;
|
||||||
complete: function (data) {
|
if (result.result == 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> Users list refreshed.', false, true, 2000, false);
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||||
},
|
users_list_table.draw();
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
} else {
|
||||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh users list.', false, true, 2000, true);
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<%
|
<%
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import common
|
from plexpy import common, helpers
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="wizard" id="some-wizard" data-title="Tautulli Setup Wizard">
|
<div class="wizard" id="setup-wizard" data-title="Tautulli Setup Wizard">
|
||||||
<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 />
|
||||||
@@ -82,22 +82,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken">
|
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="" data-validate="validatePMStoken">
|
||||||
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-card" data-cardname="card3">
|
<div class="wizard-card" data-cardname="card3">
|
||||||
<h3>Plex Media Server</h3>
|
<h3>Plex Media Server</h3>
|
||||||
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure Tautulli can reach the server.</p>
|
<p class="help-block">
|
||||||
|
Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.
|
||||||
|
</p>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<label for="pms_ip">Plex IP or Hostname</label>
|
<label for="pms_ip">Plex IP or Hostname</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-8">
|
<div class="col-xs-12">
|
||||||
<select id="pms_ip" name="pms_ip"></select>
|
<select class="form-control selectize-pms-ip" id="pms_ip" name="pms_ip">
|
||||||
|
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<label for="pms_port">Port Number</label>
|
<label for="pms_port">Plex Port</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-3">
|
<div class="col-xs-3">
|
||||||
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
||||||
@@ -105,20 +109,23 @@
|
|||||||
<div class="col-xs-4">
|
<div class="col-xs-4">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Use SSL
|
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
|
||||||
|
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-4">
|
<div class="col-xs-4">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1"> Remote Server
|
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
|
||||||
|
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
|
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
|
||||||
|
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||||
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,106 +207,6 @@
|
|||||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
|
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
|
|
||||||
$.fn.wizard.logging = false;
|
|
||||||
var options = {
|
|
||||||
keyboard : false,
|
|
||||||
contentHeight : 400,
|
|
||||||
contentWidth : 700,
|
|
||||||
backdrop: 'static',
|
|
||||||
buttons: {submitText: 'Finish'},
|
|
||||||
submitUrl: "configUpdate"
|
|
||||||
};
|
|
||||||
var wizard = $("#some-wizard").wizard(options);
|
|
||||||
wizard.show();
|
|
||||||
|
|
||||||
wizard.on("submit", function(wizard) {
|
|
||||||
// Probably should not success before we know, but hopefully validation is good enough.
|
|
||||||
wizard.submitSuccess();
|
|
||||||
$.ajax({
|
|
||||||
url: "configUpdate",
|
|
||||||
type: "POST",
|
|
||||||
url: wizard.args.submitUrl,
|
|
||||||
data: wizard.serialize(),
|
|
||||||
dataType: "json",
|
|
||||||
complete: function (data) {
|
|
||||||
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
$select_pms = $('#pms_ip').selectize({
|
|
||||||
create: true,
|
|
||||||
createOnBlur: true,
|
|
||||||
openOnFocus: true,
|
|
||||||
maxItems: 1,
|
|
||||||
closeAfterSelect: true,
|
|
||||||
onInitialize: function () {
|
|
||||||
var s = this;
|
|
||||||
this.revertSettings.$children.each(function () {
|
|
||||||
$.extend(s.options[this.value], $(this).data());
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
render: {
|
|
||||||
option: function (item, escape) {
|
|
||||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
|
||||||
},
|
|
||||||
item: function (item, escape) {
|
|
||||||
// first item is rendered before initialization bug?
|
|
||||||
if (!item.ci) {
|
|
||||||
$.extend(item,
|
|
||||||
$(this.revertSettings.$children)
|
|
||||||
.filter('[value="' + item.value + '"]').data());
|
|
||||||
|
|
||||||
}
|
|
||||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChange: function (item) {
|
|
||||||
var ci = $('.selectize-input').find('div').attr('data-ci');
|
|
||||||
var port = $('.selectize-input').find('div').attr('data-port')
|
|
||||||
var local = $('.selectize-input').find('div').attr('data-local')
|
|
||||||
var ssl = $('.selectize-input').find('div').attr('data-use_ssl')
|
|
||||||
|
|
||||||
$("#pms-verify-status").html("");
|
|
||||||
// If a option was added by a user its
|
|
||||||
// data-xxx="undefined"
|
|
||||||
if (ci != "undefined") {
|
|
||||||
// To allow next step in the guide.
|
|
||||||
// servers with clientIdentifier is verified
|
|
||||||
$("#pms_identifier").val(ci);
|
|
||||||
$("#pms_valid").val("valid");
|
|
||||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
|
|
||||||
} else {
|
|
||||||
// Self made options must be verified
|
|
||||||
$("#pms_valid").val("");
|
|
||||||
$("#pms-verify-status").html("").hide();
|
|
||||||
}
|
|
||||||
// If the server is verified set the correct port
|
|
||||||
if (port != "undefined") {
|
|
||||||
$('#pms_port').val(port);
|
|
||||||
} else {
|
|
||||||
// set default port
|
|
||||||
$('#pms_port').val("32400");
|
|
||||||
}
|
|
||||||
if (local != "undefined" && local == '0') {
|
|
||||||
$('#pms_is_remote').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
$('#pms_is_remote').prop('checked', false);
|
|
||||||
}
|
|
||||||
if (ssl != "undefined" && ssl == "1") {
|
|
||||||
$('#pms_ssl').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
$('#pms_ssl').prop('checked', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function validatePMSip(el) {
|
function validatePMSip(el) {
|
||||||
var valid_pms_ip = el.val();
|
var valid_pms_ip = el.val();
|
||||||
var retValue = {};
|
var retValue = {};
|
||||||
@@ -352,6 +259,146 @@
|
|||||||
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
|
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
$.fn.wizard.logging = false;
|
||||||
|
var options = {
|
||||||
|
keyboard : false,
|
||||||
|
contentHeight : 400,
|
||||||
|
contentWidth : 700,
|
||||||
|
backdrop: 'static',
|
||||||
|
buttons: {submitText: 'Finish'},
|
||||||
|
submitUrl: "configUpdate"
|
||||||
|
};
|
||||||
|
var wizard = $("#setup-wizard").wizard(options);
|
||||||
|
wizard.show();
|
||||||
|
|
||||||
|
// Change button classes
|
||||||
|
wizard.find('.wizard-back').addClass('btn-dark');
|
||||||
|
wizard.on('incrementCard', function(wizard) {
|
||||||
|
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
|
||||||
|
});
|
||||||
|
wizard.on('decrementCard', function(wizard) {
|
||||||
|
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
|
||||||
|
});
|
||||||
|
|
||||||
|
wizard.on("submit", function(wizard) {
|
||||||
|
// Probably should not success before we know, but hopefully validation is good enough.
|
||||||
|
wizard.submitSuccess();
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: wizard.args.submitUrl,
|
||||||
|
data: wizard.serialize(),
|
||||||
|
dataType: "json",
|
||||||
|
complete: function (data) {
|
||||||
|
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.checkbox-toggle').click(function () {
|
||||||
|
var configToggle = $(this).data('id');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$('#'+configToggle).val(1);
|
||||||
|
} else {
|
||||||
|
$('#'+configToggle).val(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var $select_pms = $('#pms_ip').selectize({
|
||||||
|
createOnBlur: true,
|
||||||
|
openOnFocus: true,
|
||||||
|
maxItems: 1,
|
||||||
|
closeAfterSelect: true,
|
||||||
|
sortField: 'label',
|
||||||
|
searchField: ['label', 'value'],
|
||||||
|
inputClass: 'form-control selectize-input',
|
||||||
|
render: {
|
||||||
|
item: function (item, escape) {
|
||||||
|
var label = item.label || item.value;
|
||||||
|
var caption = item.label ? item.value : null;
|
||||||
|
return '<div data-ssl="' + item.httpsRequired +
|
||||||
|
'" data-local="' + item.local +
|
||||||
|
'" data-identifier="' + item.clientIdentifier +
|
||||||
|
'" data-ip="' + item.ip +
|
||||||
|
'" data-port="' + item.port +
|
||||||
|
'" data-is_cloud="' + item.is_cloud +
|
||||||
|
'" data-label="' + item.label + '">' +
|
||||||
|
'<span class="item-text">' + escape(label) + '</span>' +
|
||||||
|
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
},
|
||||||
|
option: function (item, escape) {
|
||||||
|
var label = item.label || item.value;
|
||||||
|
var caption = item.label ? item.value : null;
|
||||||
|
return '<div data-ssl="' + item.httpsRequired +
|
||||||
|
'" data-local="' + item.local +
|
||||||
|
'" data-identifier="' + item.clientIdentifier +
|
||||||
|
'" data-ip="' + item.ip +
|
||||||
|
'" data-port="' + item.port +
|
||||||
|
'" data-is_cloud="' + item.is_cloud +
|
||||||
|
'" data-label="' + item.label + '">' +
|
||||||
|
escape(label) +
|
||||||
|
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: function(input) {
|
||||||
|
return {label: '', value: input};
|
||||||
|
},
|
||||||
|
onChange: function (item) {
|
||||||
|
var pms_ip_selected = this.getItem(item)[0];
|
||||||
|
var identifier = $(pms_ip_selected).data('identifier');
|
||||||
|
var port = $(pms_ip_selected).data('port');
|
||||||
|
var local = $(pms_ip_selected).data('local');
|
||||||
|
var ssl = $(pms_ip_selected).data('ssl');
|
||||||
|
var is_cloud = $(pms_ip_selected).data('is_cloud');
|
||||||
|
|
||||||
|
$("#pms_valid").val(identifier !== 'undefined' ? 'valid' : '');
|
||||||
|
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i> Server found!' : '').fadeIn('fast');
|
||||||
|
|
||||||
|
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||||
|
$('#pms_port').val(port !== 'undefined' ? port : 32400);
|
||||||
|
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||||
|
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||||
|
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||||
|
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||||
|
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
|
||||||
|
|
||||||
|
if (is_cloud === true) {
|
||||||
|
$('#pms_port').prop('readonly', true);
|
||||||
|
$('#pms_is_remote_checkbox').prop('disabled', true);
|
||||||
|
$('#pms_ssl_checkbox').prop('disabled', true);
|
||||||
|
} else {
|
||||||
|
$('#pms_port').prop('readonly', false);
|
||||||
|
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||||
|
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var select_pms = $select_pms[0].selectize;
|
||||||
|
|
||||||
|
function getServerOptions(token) {
|
||||||
|
/* Set token and returns server options */
|
||||||
|
$.ajax({
|
||||||
|
url: 'discover',
|
||||||
|
data: {
|
||||||
|
token: token
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result) {
|
||||||
|
var existing_value = $('#pms_ip').val();
|
||||||
|
result.forEach(function (item) {
|
||||||
|
if (item.value === existing_value) {
|
||||||
|
select_pms.updateOption(item.value, item);
|
||||||
|
} else {
|
||||||
|
select_pms.addOption(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
var pms_verified = false;
|
var pms_verified = false;
|
||||||
var authenticated = false;
|
var authenticated = false;
|
||||||
|
|
||||||
@@ -360,14 +407,19 @@
|
|||||||
var pms_ip = $("#pms_ip").val().trim();
|
var pms_ip = $("#pms_ip").val().trim();
|
||||||
var pms_port = $("#pms_port").val().trim();
|
var pms_port = $("#pms_port").val().trim();
|
||||||
var pms_identifier = $("#pms_identifier").val();
|
var pms_identifier = $("#pms_identifier").val();
|
||||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
var pms_ssl = $("#pms_ssl").val();
|
||||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
var pms_is_remote = $("#pms_is_remote").val();
|
||||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
||||||
$('#pms-verify-status').fadeIn('fast');
|
$('#pms-verify-status').fadeIn('fast');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_server_id',
|
url: 'get_server_id',
|
||||||
data: { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
|
data: {
|
||||||
|
hostname: pms_ip,
|
||||||
|
port: pms_port,
|
||||||
|
identifier: pms_identifier,
|
||||||
|
ssl: pms_ssl,
|
||||||
|
remote: pms_is_remote },
|
||||||
cache: true,
|
cache: true,
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -444,39 +496,7 @@
|
|||||||
$('#pms-token-status').fadeIn('fast');
|
$('#pms-token-status').fadeIn('fast');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// Send database path to import script
|
|
||||||
//$("#plexwatch-import").click(function() {
|
|
||||||
// var database_path = $("#db_location").val();
|
|
||||||
// var table_name = 'processed';
|
|
||||||
// var import_ignore_interval = 0;
|
|
||||||
// $.ajax({
|
|
||||||
// url: 'get_plexwatch_export_data',
|
|
||||||
// data: {database_path: database_path, table_name:table_name, import_ignore_interval:import_ignore_interval},
|
|
||||||
// cache: false,
|
|
||||||
// async: true,
|
|
||||||
// success: function(data) {
|
|
||||||
// if (data === 'Import has started. Check the Tautulli logs to monitor any problems.') {
|
|
||||||
// $("#plexwatch-import-status").html('Started');
|
|
||||||
// } else {
|
|
||||||
// $("#plexwatch-import-status").html(data);
|
|
||||||
// }
|
|
||||||
// $("#db_location").val('')
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//});
|
|
||||||
|
|
||||||
function getServerOptions(token) {
|
|
||||||
/* Set token and returns server options */
|
|
||||||
$.ajax({
|
|
||||||
url: "discover/" + token,
|
|
||||||
success: function (result) {
|
|
||||||
$('#pms_ip').html("");
|
|
||||||
// Add all servers to the "combobox"
|
|
||||||
$select_pms[0].selectize.addOption(result);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
121
lib/UniversalAnalytics/HTTPLog.py
Normal file
121
lib/UniversalAnalytics/HTTPLog.py
Normal 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
|
433
lib/UniversalAnalytics/Tracker.py
Normal file
433
lib/UniversalAnalytics/Tracker.py
Normal 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
|
1
lib/UniversalAnalytics/__init__.py
Normal file
1
lib/UniversalAnalytics/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import Tracker
|
@@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
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
|
||||||
import threading
|
import threading
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Some cut down versions of Python may not include this module and it's not critical for us
|
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||||
try:
|
try:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -32,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
|
||||||
@@ -54,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
|
||||||
@@ -71,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
|
||||||
|
|
||||||
@@ -84,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
|
||||||
|
|
||||||
@@ -92,7 +96,9 @@ HTTP_ROOT = None
|
|||||||
DEV = False
|
DEV = False
|
||||||
|
|
||||||
WS_CONNECTED = False
|
WS_CONNECTED = False
|
||||||
PLEX_SERVER_UP = True
|
PLEX_SERVER_UP = None
|
||||||
|
|
||||||
|
TRACKER = None
|
||||||
|
|
||||||
|
|
||||||
def initialize(config_file):
|
def initialize(config_file):
|
||||||
@@ -105,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
|
||||||
@@ -157,16 +164,6 @@ def initialize(config_file):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e))
|
logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e))
|
||||||
|
|
||||||
if CONFIG.CACHE_DIR:
|
|
||||||
session_metadata_folder = os.path.join(CONFIG.CACHE_DIR, 'session_metadata')
|
|
||||||
try:
|
|
||||||
shutil.rmtree(session_metadata_folder, ignore_errors=True)
|
|
||||||
except OSError as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not os.path.exists(session_metadata_folder):
|
|
||||||
os.mkdir(session_metadata_folder)
|
|
||||||
|
|
||||||
# Initialize the database
|
# Initialize the database
|
||||||
logger.info(u"Checking if the database upgrades are required...")
|
logger.info(u"Checking if the database upgrades are required...")
|
||||||
try:
|
try:
|
||||||
@@ -213,6 +210,8 @@ def initialize(config_file):
|
|||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(u"Unable to read previous version from file '%s': %s" %
|
logger.error(u"Unable to read previous version from file '%s': %s" %
|
||||||
(version_lock_file, e))
|
(version_lock_file, e))
|
||||||
|
else:
|
||||||
|
prev_version = 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca'
|
||||||
|
|
||||||
# Get the currently installed version. Returns None, 'win32' or the git
|
# Get the currently installed version. Returns None, 'win32' or the git
|
||||||
# hash.
|
# hash.
|
||||||
@@ -256,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:
|
||||||
@@ -284,6 +284,7 @@ def initialize(config_file):
|
|||||||
_INITIALIZED = True
|
_INITIALIZED = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def daemonize():
|
def daemonize():
|
||||||
if threading.activeCount() != 1:
|
if threading.activeCount() != 1:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -393,7 +394,7 @@ def initialize_scheduler():
|
|||||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=library_hours, minutes=0, seconds=0)
|
hours=library_hours, minutes=0, seconds=0)
|
||||||
|
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -411,12 +412,9 @@ def initialize_scheduler():
|
|||||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
# Schedule job to reconnect websocket
|
# Schedule job to reconnect server
|
||||||
response_seconds = CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS * CONFIG.WEBSOCKET_CONNECTION_TIMEOUT
|
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||||
response_seconds = 60 if response_seconds < 60 else response_seconds
|
hours=0, minutes=0, seconds=60, args=(False,))
|
||||||
|
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
|
||||||
hours=0, minutes=0, seconds=response_seconds)
|
|
||||||
|
|
||||||
# Start scheduler
|
# Start scheduler
|
||||||
if start_jobs and len(SCHED.get_jobs()):
|
if start_jobs and len(SCHED.get_jobs()):
|
||||||
@@ -460,6 +458,22 @@ def start():
|
|||||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||||
notifiers.check_browser_enabled()
|
notifiers.check_browser_enabled()
|
||||||
|
|
||||||
|
if CONFIG.FIRST_RUN_COMPLETE:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -1080,9 +1094,9 @@ def dbcheck():
|
|||||||
)
|
)
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'UPDATE session_history_media_info SET transcode_decision = (CASE '
|
'UPDATE session_history_media_info SET transcode_decision = (CASE '
|
||||||
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
||||||
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
||||||
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Upgrade session_history_media_info table from earlier versions
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
@@ -1241,7 +1255,6 @@ def dbcheck():
|
|||||||
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
|
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Upgrade session_history_media_info table from earlier versions
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
try:
|
try:
|
||||||
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
||||||
@@ -1586,24 +1599,26 @@ def dbcheck():
|
|||||||
if not result.fetchone():
|
if not result.fetchone():
|
||||||
logger.debug(u"User 'Local' does not exist. Adding user.")
|
logger.debug(u"User 'Local' does not exist. Adding user.")
|
||||||
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
|
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
|
||||||
|
|
||||||
# Create table indices
|
# Create table indices
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup ON tvmaze_lookup (rating_key)'
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup ON tvmaze_lookup (rating_key)'
|
||||||
)
|
)
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_themoviedb_lookup ON themoviedb_lookup (rating_key)'
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_themoviedb_lookup ON themoviedb_lookup (rating_key)'
|
||||||
)
|
)
|
||||||
|
|
||||||
conn_db.commit()
|
conn_db.commit()
|
||||||
c_db.close()
|
c_db.close()
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
if CONFIG.UPDATE_NOTIFIERS_DB:
|
if CONFIG.UPDATE_NOTIFIERS_DB:
|
||||||
notifiers.upgrade_config_to_db()
|
notifiers.upgrade_config_to_db()
|
||||||
if CONFIG.UPDATE_LIBRARIES_DB_NOTIFY:
|
if CONFIG.UPDATE_LIBRARIES_DB_NOTIFY:
|
||||||
libraries.update_libraries_db_notify()
|
libraries.update_libraries_db_notify()
|
||||||
|
|
||||||
|
|
||||||
def shutdown(restart=False, update=False, checkout=False):
|
def shutdown(restart=False, update=False, checkout=False):
|
||||||
cherrypy.engine.exit()
|
cherrypy.engine.exit()
|
||||||
SCHED.shutdown(wait=False)
|
SCHED.shutdown(wait=False)
|
||||||
@@ -1672,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)
|
||||||
|
@@ -273,14 +273,25 @@ class ActivityHandler(object):
|
|||||||
# Monitor if the stream has reached the watch percentage for notifications
|
# Monitor if the stream has reached the watch percentage for notifications
|
||||||
# The only purpose of this is for notifications
|
# The only purpose of this is for notifications
|
||||||
if this_state != 'buffering':
|
if this_state != 'buffering':
|
||||||
progress_percent = helpers.get_percent(db_session['view_offset'], db_session['duration'])
|
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||||
notify_states = notification_handler.get_notify_state(session=db_session)
|
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||||
if (db_session['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
}
|
||||||
logger.debug(u"Tautulli ActivityHandler :: Session %s watched." % str(self.get_session_key()))
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_watched'})
|
if progress_percent >= watched_percent.get(db_session['media_type'], 101):
|
||||||
|
watched_notifiers = notification_handler.get_notify_state_enabled(
|
||||||
|
session=db_session, notify_action='on_watched', notified=False)
|
||||||
|
|
||||||
|
if watched_notifiers:
|
||||||
|
logger.debug(u"Tautulli ActivityHandler :: Session %s watched."
|
||||||
|
% str(self.get_session_key()))
|
||||||
|
|
||||||
|
for d in watched_notifiers:
|
||||||
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
||||||
|
'notifier_id': d['notifier_id'],
|
||||||
|
'notify_action': 'on_watched'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# We don't have this session in our table yet, start a new one.
|
# We don't have this session in our table yet, start a new one.
|
||||||
|
@@ -264,12 +264,37 @@ def check_recently_added():
|
|||||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||||
|
|
||||||
|
|
||||||
def check_server_response():
|
def connect_server(log=True, startup=False):
|
||||||
logger.info(u"Tautulli Monitor :: Attempting to reconnect Plex server...")
|
if plexpy.CONFIG.PMS_IS_CLOUD:
|
||||||
try:
|
if log:
|
||||||
web_socket.start_thread()
|
logger.info(u"Tautulli Monitor :: Checking for Plex Cloud server status...")
|
||||||
except:
|
|
||||||
logger.warn(u"Websocket :: Unable to open connection.")
|
plex_tv = plextv.PlexTV()
|
||||||
|
status = plex_tv.get_cloud_server_status()
|
||||||
|
|
||||||
|
if status is True:
|
||||||
|
logger.info(u"Tautulli Monitor :: Plex Cloud server is active.")
|
||||||
|
elif status is False:
|
||||||
|
if log:
|
||||||
|
logger.info(u"Tautulli Monitor :: Plex Cloud server is sleeping.")
|
||||||
|
else:
|
||||||
|
if log:
|
||||||
|
logger.error(u"Tautulli Monitor :: Failed to retrieve Plex Cloud server status.")
|
||||||
|
|
||||||
|
if not status and startup:
|
||||||
|
web_socket.on_disconnect()
|
||||||
|
|
||||||
|
else:
|
||||||
|
status = True
|
||||||
|
|
||||||
|
if status:
|
||||||
|
if log and not startup:
|
||||||
|
logger.info(u"Tautulli Monitor :: Attempting to reconnect Plex server...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
web_socket.start_thread()
|
||||||
|
except:
|
||||||
|
logger.error(u"Websocket :: Unable to open connection.")
|
||||||
|
|
||||||
|
|
||||||
def check_server_access():
|
def check_server_access():
|
||||||
@@ -325,4 +350,4 @@ def check_server_updates():
|
|||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info(u"Tautulli Monitor :: No PMS update available.")
|
logger.info(u"Tautulli Monitor :: No PMS update available.")
|
||||||
|
@@ -335,14 +335,14 @@ class API2:
|
|||||||
""" Restart Tautulli."""
|
""" Restart Tautulli."""
|
||||||
|
|
||||||
plexpy.SIGNAL = 'restart'
|
plexpy.SIGNAL = 'restart'
|
||||||
self._api_msg = 'Restarting plexpy'
|
self._api_msg = 'Restarting Tautulli'
|
||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
""" Check for Tautulli updates on Github."""
|
""" Update Tautulli."""
|
||||||
|
|
||||||
plexpy.SIGNAL = 'update'
|
plexpy.SIGNAL = 'update'
|
||||||
self._api_msg = 'Updating plexpy'
|
self._api_msg = 'Updating Tautulli'
|
||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
|
|
||||||
def refresh_libraries_list(self, **kwargs):
|
def refresh_libraries_list(self, **kwargs):
|
||||||
|
@@ -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.'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@@ -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']
|
||||||
|
@@ -188,7 +188,7 @@ class DataFactory(object):
|
|||||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
'photo': 0,
|
'photo': 0,
|
||||||
'clip': plexpy.CONFIG.MOVIE_WATCHED_PERCENT
|
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||||
}
|
}
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
@@ -612,7 +612,6 @@ class DataFactory(object):
|
|||||||
'total_plays': item['total_plays'],
|
'total_plays': item['total_plays'],
|
||||||
'total_duration': item['total_duration'],
|
'total_duration': item['total_duration'],
|
||||||
'last_play': item['last_watch'],
|
'last_play': item['last_watch'],
|
||||||
'thumb': user_thumb,
|
|
||||||
'user_thumb': user_thumb,
|
'user_thumb': user_thumb,
|
||||||
'grandparent_thumb': '',
|
'grandparent_thumb': '',
|
||||||
'art': '',
|
'art': '',
|
||||||
@@ -827,6 +826,9 @@ class DataFactory(object):
|
|||||||
if session.get_session_shared_libraries():
|
if session.get_session_shared_libraries():
|
||||||
library_cards = session.get_session_shared_libraries()
|
library_cards = session.get_session_shared_libraries()
|
||||||
|
|
||||||
|
if 'first_run_wizard' in library_cards:
|
||||||
|
return None
|
||||||
|
|
||||||
library_stats = []
|
library_stats = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
204
plexpy/graphs.py
204
plexpy/graphs.py
@@ -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)
|
||||||
|
|
||||||
|
@@ -39,11 +39,17 @@ class HTTPHandler(object):
|
|||||||
else:
|
else:
|
||||||
self.urls = urls
|
self.urls = urls
|
||||||
|
|
||||||
|
self.headers = {'X-Plex-Device-Name': 'Tautulli',
|
||||||
|
'X-Plex-Product': 'Tautulli',
|
||||||
|
'X-Plex-Version': plexpy.common.VERSION_NUMBER,
|
||||||
|
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||||
|
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
||||||
|
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||||
|
}
|
||||||
|
|
||||||
self.token = token
|
self.token = token
|
||||||
if self.token:
|
if self.token:
|
||||||
self.headers = {'X-Plex-Token': self.token}
|
self.headers['X-Plex-Token'] = self.token
|
||||||
else:
|
|
||||||
self.headers = {}
|
|
||||||
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.ssl_verify = ssl_verify
|
self.ssl_verify = ssl_verify
|
||||||
@@ -79,9 +85,9 @@ class HTTPHandler(object):
|
|||||||
if uri:
|
if uri:
|
||||||
request_urls = [urljoin(url, self.uri) for url in self.urls]
|
request_urls = [urljoin(url, self.uri) for url in self.urls]
|
||||||
|
|
||||||
if no_token and headers:
|
if no_token:
|
||||||
self.headers = headers
|
self.headers.pop('X-Plex-Token', None)
|
||||||
elif headers:
|
if headers:
|
||||||
self.headers.update(headers)
|
self.headers.update(headers)
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
|
@@ -1006,13 +1006,13 @@ class Libraries(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)
|
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)
|
||||||
|
|
||||||
def delete_datatable_media_info_cache(self, section_id=None):
|
def delete_media_info_cache(self, section_id=None):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if section_id.isdigit():
|
if section_id.isdigit():
|
||||||
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
||||||
if f.startswith('media_info-%s' % section_id) and f.endswith('.json')]
|
if f.startswith('media_info_%s' % section_id) and f.endswith('.json')]
|
||||||
|
|
||||||
logger.debug(u"Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
logger.debug(u"Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
||||||
return 'Deleted media info table cache for library with id %s.' % section_id
|
return 'Deleted media info table cache for library with id %s.' % section_id
|
||||||
|
@@ -82,11 +82,6 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
|
|||||||
# Check if any notification agents have notifications enabled for the action
|
# Check if any notification agents have notifications enabled for the action
|
||||||
notifiers_enabled = notifiers.get_notifiers(notify_action=notify_action)
|
notifiers_enabled = notifiers.get_notifiers(notify_action=notify_action)
|
||||||
|
|
||||||
# Check if the watched notifications has already been sent
|
|
||||||
if stream_data and notify_action == 'on_watched':
|
|
||||||
watched_notifiers = [d['notifier_id'] for d in get_notify_state(session=stream_data)]
|
|
||||||
notifiers_enabled = [n for n in notifiers_enabled if n['id'] not in watched_notifiers]
|
|
||||||
|
|
||||||
if notifiers_enabled and not manual_trigger:
|
if notifiers_enabled and not manual_trigger:
|
||||||
# Check if notification conditions are satisfied
|
# Check if notification conditions are satisfied
|
||||||
conditions = notify_conditions(notify_action=notify_action,
|
conditions = notify_conditions(notify_action=notify_action,
|
||||||
@@ -390,6 +385,28 @@ def get_notify_state(session):
|
|||||||
return notify_states
|
return notify_states
|
||||||
|
|
||||||
|
|
||||||
|
def get_notify_state_enabled(session, notify_action, notified=True):
|
||||||
|
if notified:
|
||||||
|
timestamp_where = 'AND timestamp IS NOT NULL'
|
||||||
|
else:
|
||||||
|
timestamp_where = 'AND timestamp IS NULL'
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
result = monitor_db.select('SELECT id AS notifier_id, timestamp '
|
||||||
|
'FROM notifiers '
|
||||||
|
'LEFT OUTER JOIN ('
|
||||||
|
'SELECT timestamp, notifier_id '
|
||||||
|
'FROM notify_log '
|
||||||
|
'WHERE session_key = ? '
|
||||||
|
'AND rating_key = ? '
|
||||||
|
'AND user_id = ? '
|
||||||
|
'AND notify_action = ?) AS t ON notifiers.id = t.notifier_id '
|
||||||
|
'WHERE %s = 1 %s' % (notify_action, timestamp_where),
|
||||||
|
args=[session['session_key'], session['rating_key'], session['user_id'], notify_action])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
||||||
|
|
||||||
if notifier and notify_action:
|
if notifier and notify_action:
|
||||||
@@ -890,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', ''),
|
||||||
|
@@ -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
|
||||||
|
@@ -144,14 +144,7 @@ class PlexTV(object):
|
|||||||
uri = '/users/sign_in.xml'
|
uri = '/users/sign_in.xml'
|
||||||
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
||||||
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
||||||
'X-Plex-Device-Name': 'Tautulli',
|
'Authorization': 'Basic %s' % base64string}
|
||||||
'X-Plex-Product': 'Tautulli',
|
|
||||||
'X-Plex-Version': plexpy.common.VERSION_NUMBER,
|
|
||||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
|
||||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
|
||||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
|
||||||
'Authorization': 'Basic %s' % base64string
|
|
||||||
}
|
|
||||||
|
|
||||||
request = self.request_handler.make_request(uri=uri,
|
request = self.request_handler.make_request(uri=uri,
|
||||||
request_type='POST',
|
request_type='POST',
|
||||||
@@ -318,6 +311,14 @@ class PlexTV(object):
|
|||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
def cloud_server_status(self, output_format=''):
|
||||||
|
uri = '/api/v2/cloud_server'
|
||||||
|
request = self.request_handler.make_request(uri=uri,
|
||||||
|
request_type='GET',
|
||||||
|
output_format=output_format)
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
||||||
def get_full_users_list(self):
|
def get_full_users_list(self):
|
||||||
friends_list = self.get_plextv_friends(output_format='xml')
|
friends_list = self.get_plextv_friends(output_format='xml')
|
||||||
own_account = self.get_plextv_user_details(output_format='xml')
|
own_account = self.get_plextv_user_details(output_format='xml')
|
||||||
@@ -645,7 +646,8 @@ class PlexTV(object):
|
|||||||
'ip': helpers.get_xml_attr(c, 'address'),
|
'ip': helpers.get_xml_attr(c, 'address'),
|
||||||
'port': helpers.get_xml_attr(c, 'port'),
|
'port': helpers.get_xml_attr(c, 'port'),
|
||||||
'local': helpers.get_xml_attr(c, 'local'),
|
'local': helpers.get_xml_attr(c, 'local'),
|
||||||
'value': helpers.get_xml_attr(c, 'address')
|
'value': helpers.get_xml_attr(c, 'address'),
|
||||||
|
'is_cloud': is_cloud
|
||||||
}
|
}
|
||||||
clean_servers.append(server)
|
clean_servers.append(server)
|
||||||
|
|
||||||
@@ -753,3 +755,21 @@ class PlexTV(object):
|
|||||||
devices_list.append(device)
|
devices_list.append(device)
|
||||||
|
|
||||||
return devices_list
|
return devices_list
|
||||||
|
|
||||||
|
def get_cloud_server_status(self):
|
||||||
|
cloud_status = self.cloud_server_status(output_format='xml')
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_info = cloud_status.getElementsByTagName('info')
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_cloud_server_status: %s." % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for info in status_info:
|
||||||
|
servers = info.getElementsByTagName('server')
|
||||||
|
for s in servers:
|
||||||
|
if helpers.get_xml_attr(s, 'address') == plexpy.CONFIG.PMS_IP:
|
||||||
|
if helpers.get_xml_attr(info, 'running') == '1':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
@@ -61,7 +61,7 @@ class PmsConnect(object):
|
|||||||
self.url = plexpy.CONFIG.PMS_URL
|
self.url = plexpy.CONFIG.PMS_URL
|
||||||
elif not self.url:
|
elif not self.url:
|
||||||
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
||||||
port=plexpy.CONFIG.PMS_PORT)
|
port=plexpy.CONFIG.PMS_PORT)
|
||||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
||||||
|
|
||||||
if not self.token:
|
if not self.token:
|
||||||
@@ -533,7 +533,12 @@ class PmsConnect(object):
|
|||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
||||||
if cache_key:
|
if cache_key:
|
||||||
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
|
in_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||||
|
in_file_path = os.path.join(in_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
|
|
||||||
|
if not os.path.exists(in_file_folder):
|
||||||
|
os.mkdir(in_file_folder)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(in_file_path, 'r') as inFile:
|
with open(in_file_path, 'r') as inFile:
|
||||||
metadata = json.load(inFile)
|
metadata = json.load(inFile)
|
||||||
@@ -1179,7 +1184,12 @@ class PmsConnect(object):
|
|||||||
if cache_key:
|
if cache_key:
|
||||||
metadata['_cache_time'] = int(time.time())
|
metadata['_cache_time'] = int(time.time())
|
||||||
|
|
||||||
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
|
out_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||||
|
out_file_path = os.path.join(out_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
|
|
||||||
|
if not os.path.exists(out_file_folder):
|
||||||
|
os.mkdir(out_file_folder)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(out_file_path, 'w') as outFile:
|
with open(out_file_path, 'w') as outFile:
|
||||||
json.dump(metadata, outFile)
|
json.dump(metadata, outFile)
|
||||||
|
@@ -191,11 +191,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
|||||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||||
'ip_address': 'N/A',
|
'ip_address': 'N/A',
|
||||||
'machine_id': '',
|
'machine_id': '',
|
||||||
'platform': 'Platform',
|
'player': 'Player'
|
||||||
'player': 'Player',
|
|
||||||
'quality_profile': 'Unknown',
|
|
||||||
'bandwidth': '',
|
|
||||||
'location': ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata_to_mask = {'media_index': '0',
|
metadata_to_mask = {'media_index': '0',
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "beta"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.0.18-beta"
|
PLEXPY_RELEASE_VERSION = "v2.0.20-beta"
|
||||||
|
@@ -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})
|
||||||
|
|
||||||
|
@@ -33,14 +33,33 @@ ws_reconnect = False
|
|||||||
|
|
||||||
|
|
||||||
def start_thread():
|
def start_thread():
|
||||||
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
|
# Check for any existing sessions on start up
|
||||||
# Check for any existing sessions on start up
|
activity_pinger.check_active_sessions(ws_request=True)
|
||||||
activity_pinger.check_active_sessions(ws_request=True)
|
# Start the websocket listener on it's own thread
|
||||||
# Start the websocket listener on it's own thread
|
threading.Thread(target=run).start()
|
||||||
threading.Thread(target=run).start()
|
|
||||||
|
|
||||||
|
def on_connect():
|
||||||
|
if plexpy.PLEX_SERVER_UP is None:
|
||||||
|
plexpy.PLEX_SERVER_UP = True
|
||||||
|
|
||||||
|
if not plexpy.PLEX_SERVER_UP:
|
||||||
|
logger.info(u"Tautulli WebSocket :: The Plex Media Server is back up.")
|
||||||
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
||||||
|
plexpy.PLEX_SERVER_UP = True
|
||||||
|
|
||||||
|
plexpy.initialize_scheduler()
|
||||||
|
|
||||||
|
|
||||||
def on_disconnect():
|
def on_disconnect():
|
||||||
|
if plexpy.PLEX_SERVER_UP is None:
|
||||||
|
plexpy.PLEX_SERVER_UP = False
|
||||||
|
|
||||||
|
if plexpy.PLEX_SERVER_UP:
|
||||||
|
logger.info(u"Tautulli WebSocket :: Unable to get a response from the server, Plex server is down.")
|
||||||
|
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
||||||
|
plexpy.PLEX_SERVER_UP = False
|
||||||
|
|
||||||
activity_processor.ActivityProcessor().set_temp_stopped()
|
activity_processor.ActivityProcessor().set_temp_stopped()
|
||||||
plexpy.initialize_scheduler()
|
plexpy.initialize_scheduler()
|
||||||
|
|
||||||
@@ -55,7 +74,7 @@ def run():
|
|||||||
|
|
||||||
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
||||||
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||||
secure = ' secure'
|
secure = 'secure '
|
||||||
else:
|
else:
|
||||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||||
plexpy.CONFIG.PMS_IP,
|
plexpy.CONFIG.PMS_IP,
|
||||||
@@ -72,34 +91,29 @@ def run():
|
|||||||
global ws_reconnect
|
global ws_reconnect
|
||||||
ws_reconnect = False
|
ws_reconnect = False
|
||||||
reconnects = 0
|
reconnects = 0
|
||||||
ws_exception = False
|
|
||||||
|
|
||||||
# Try an open the websocket connection
|
# Try an open the websocket connection
|
||||||
while not plexpy.WS_CONNECTED and reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
while not plexpy.WS_CONNECTED and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||||
try:
|
if reconnects == 0:
|
||||||
logger.info(u"Tautulli WebSocket :: Opening%s websocket, connection attempt %s." % (secure, str(reconnects + 1)))
|
logger.info(u"Tautulli WebSocket :: Opening %swebsocket." % secure)
|
||||||
ws = create_connection(uri, header=header)
|
|
||||||
reconnects = 0
|
|
||||||
logger.info(u"Tautulli WebSocket :: Ready")
|
|
||||||
plexpy.WS_CONNECTED = True
|
|
||||||
|
|
||||||
if not plexpy.PLEX_SERVER_UP:
|
reconnects += 1
|
||||||
logger.info(u"Tautulli WebSocket :: The Plex Media Server is back up.")
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
|
||||||
plexpy.PLEX_SERVER_UP = True
|
|
||||||
|
|
||||||
plexpy.initialize_scheduler()
|
# Sleep 5 between connection attempts
|
||||||
|
if reconnects > 1:
|
||||||
except IOError as e:
|
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
|
||||||
reconnects += 1
|
|
||||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||||
|
|
||||||
except (websocket.WebSocketException, Exception) as e:
|
logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws = create_connection(uri, header=header)
|
||||||
|
logger.info(u"Tautulli WebSocket :: Ready")
|
||||||
|
plexpy.WS_CONNECTED = True
|
||||||
|
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
plexpy.WS_CONNECTED = False
|
|
||||||
ws_exception = True
|
if plexpy.WS_CONNECTED:
|
||||||
break
|
on_connect()
|
||||||
|
|
||||||
while plexpy.WS_CONNECTED:
|
while plexpy.WS_CONNECTED:
|
||||||
try:
|
try:
|
||||||
@@ -109,20 +123,24 @@ def run():
|
|||||||
reconnects = 0
|
reconnects = 0
|
||||||
|
|
||||||
except websocket.WebSocketConnectionClosedException:
|
except websocket.WebSocketConnectionClosedException:
|
||||||
if reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
if reconnects == 0:
|
||||||
|
logger.warn(u"Tautulli WebSocket :: Connection has closed.")
|
||||||
|
|
||||||
|
if not plexpy.CONFIG.PMS_IS_CLOUD and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||||
reconnects += 1
|
reconnects += 1
|
||||||
|
|
||||||
# Sleep 5 between connection attempts
|
# Sleep 5 between connection attempts
|
||||||
if reconnects > 1:
|
if reconnects > 1:
|
||||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||||
|
|
||||||
logger.warn(u"Tautulli WebSocket :: Connection has closed, reconnection attempt %s." % reconnects)
|
logger.warn(u"Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ws = create_connection(uri, header=header)
|
ws = create_connection(uri, header=header)
|
||||||
logger.info(u"Tautulli WebSocket :: Ready")
|
logger.info(u"Tautulli WebSocket :: Ready")
|
||||||
plexpy.WS_CONNECTED = True
|
plexpy.WS_CONNECTED = True
|
||||||
except IOError as e:
|
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||||
logger.info(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ws.shutdown()
|
ws.shutdown()
|
||||||
@@ -131,8 +149,8 @@ def run():
|
|||||||
|
|
||||||
except (websocket.WebSocketException, Exception) as e:
|
except (websocket.WebSocketException, Exception) as e:
|
||||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||||
|
ws.shutdown()
|
||||||
plexpy.WS_CONNECTED = False
|
plexpy.WS_CONNECTED = False
|
||||||
ws_exception = True
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if we recieved a restart notification and close websocket connection cleanly
|
# Check if we recieved a restart notification and close websocket connection cleanly
|
||||||
@@ -143,13 +161,6 @@ def run():
|
|||||||
start_thread()
|
start_thread()
|
||||||
|
|
||||||
if not plexpy.WS_CONNECTED and not ws_reconnect:
|
if not plexpy.WS_CONNECTED and not ws_reconnect:
|
||||||
logger.error(u"Tautulli WebSocket :: Connection unavailable.")
|
|
||||||
|
|
||||||
if not ws_exception and plexpy.PLEX_SERVER_UP:
|
|
||||||
logger.info(u"Tautulli WebSocket :: Unable to get an internal response from the server, Plex server is down.")
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
|
||||||
plexpy.PLEX_SERVER_UP = False
|
|
||||||
|
|
||||||
on_disconnect()
|
on_disconnect()
|
||||||
|
|
||||||
logger.debug(u"Tautulli WebSocket :: Leaving thread.")
|
logger.debug(u"Tautulli WebSocket :: Leaving thread.")
|
||||||
|
@@ -28,6 +28,7 @@ from mako.lookup import TemplateLookup
|
|||||||
from mako import exceptions
|
from mako import exceptions
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
|
import activity_pinger
|
||||||
import common
|
import common
|
||||||
import config
|
import config
|
||||||
import database
|
import database
|
||||||
@@ -98,10 +99,11 @@ class WebInterface(object):
|
|||||||
config = {
|
config = {
|
||||||
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
"pms_ip": plexpy.CONFIG.PMS_IP,
|
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
|
||||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||||
|
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
|
||||||
|
"pms_ssl": plexpy.CONFIG.PMS_SSL,
|
||||||
|
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
|
||||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||||
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL
|
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,7 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi("get_server_list")
|
@addtoapi("get_server_list")
|
||||||
def discover(self, token=None, include_cloud=True, all_servers=False, **kwargs):
|
def discover(self, token=None, include_cloud=True, all_servers=True, **kwargs):
|
||||||
""" Get all your servers that are published to Plex.tv.
|
""" Get all your servers that are published to Plex.tv.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -148,7 +150,7 @@ class WebInterface(object):
|
|||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
include_cloud = not (include_cloud == 'false')
|
include_cloud = not (include_cloud == 'false')
|
||||||
all_servers = (all_servers == 'true')
|
all_servers = not (all_servers == 'false')
|
||||||
|
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
servers_list = plex_tv.discover(include_cloud=include_cloud,
|
servers_list = plex_tv.discover(include_cloud=include_cloud,
|
||||||
@@ -456,12 +458,17 @@ class WebInterface(object):
|
|||||||
logger.warn(u"Unable to retrieve data for get_library_sections.")
|
logger.warn(u"Unable to retrieve data for get_library_sections.")
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def refresh_libraries_list(self, **kwargs):
|
def refresh_libraries_list(self, **kwargs):
|
||||||
""" Refresh the libraries list on it's own thread. """
|
""" Manually refresh the libraries list. """
|
||||||
threading.Thread(target=libraries.refresh_libraries).start()
|
|
||||||
logger.info(u"Manual libraries list refresh requested.")
|
logger.info(u"Manual libraries list refresh requested.")
|
||||||
return True
|
result = libraries.refresh_libraries()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Libraries list refreshed.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Unable to refresh libraries list.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
@@ -629,6 +636,7 @@ class WebInterface(object):
|
|||||||
start (int): Row to start from, 0
|
start (int): Row to start from, 0
|
||||||
length (int): Number of items to return, 25
|
length (int): Number of items to return, 25
|
||||||
search (str): A string to search for, "Thrones"
|
search (str): A string to search for, "Thrones"
|
||||||
|
refresh (str): "true" to refresh the media info table
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
@@ -952,7 +960,7 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def delete_datatable_media_info_cache(self, section_id, **kwargs):
|
def delete_media_info_cache(self, section_id, **kwargs):
|
||||||
""" Delete the media info table cache for a specific library.
|
""" Delete the media info table cache for a specific library.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -972,7 +980,7 @@ class WebInterface(object):
|
|||||||
if section_id not in section_ids:
|
if section_id not in section_ids:
|
||||||
if section_id:
|
if section_id:
|
||||||
library_data = libraries.Libraries()
|
library_data = libraries.Libraries()
|
||||||
delete_row = library_data.delete_datatable_media_info_cache(section_id=section_id)
|
delete_row = library_data.delete_media_info_cache(section_id=section_id)
|
||||||
|
|
||||||
if delete_row:
|
if delete_row:
|
||||||
return {'message': delete_row}
|
return {'message': delete_row}
|
||||||
@@ -1076,12 +1084,17 @@ class WebInterface(object):
|
|||||||
return user_list
|
return user_list
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def refresh_users_list(self, **kwargs):
|
def refresh_users_list(self, **kwargs):
|
||||||
""" Refresh the users list on it's own thread. """
|
""" Manually refresh the users list. """
|
||||||
threading.Thread(target=users.refresh_users).start()
|
|
||||||
logger.info(u"Manual users list refresh requested.")
|
logger.info(u"Manual users list refresh requested.")
|
||||||
return True
|
result = users.refresh_users()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return {'result': 'success', 'message': 'Users list refreshed.'}
|
||||||
|
else:
|
||||||
|
return {'result': 'error', 'message': 'Unable to refresh users list.'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
@@ -1786,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1797,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:
|
||||||
@@ -1810,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
|
||||||
@@ -1822,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1833,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:
|
||||||
@@ -1846,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)
|
||||||
|
|
||||||
@@ -1858,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1869,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:
|
||||||
@@ -1882,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)
|
||||||
|
|
||||||
@@ -1894,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1905,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:
|
||||||
@@ -1918,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)
|
||||||
|
|
||||||
@@ -1930,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1941,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:
|
||||||
@@ -1954,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)
|
||||||
|
|
||||||
@@ -1966,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1977,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:
|
||||||
@@ -1990,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)
|
||||||
|
|
||||||
@@ -2002,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -2013,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:
|
||||||
@@ -2026,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)
|
||||||
|
|
||||||
@@ -2038,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -2049,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:
|
||||||
@@ -2062,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)
|
||||||
|
|
||||||
@@ -2074,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -2085,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:
|
||||||
@@ -2098,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)
|
||||||
|
|
||||||
@@ -2110,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -2121,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:
|
||||||
@@ -2134,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)
|
||||||
|
|
||||||
@@ -2146,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.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -2157,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:
|
||||||
@@ -2170,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)
|
||||||
|
|
||||||
@@ -2562,8 +2608,8 @@ class WebInterface(object):
|
|||||||
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
|
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
|
||||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
"pms_ssl": plexpy.CONFIG.PMS_SSL,
|
||||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
|
||||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||||
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
|
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
|
||||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||||
@@ -2623,7 +2669,7 @@ class WebInterface(object):
|
|||||||
checked_configs = [
|
checked_configs = [
|
||||||
"launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
|
"launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
|
||||||
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
|
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
|
||||||
"pms_ssl", "pms_is_remote", "pms_url_manual", "week_start_monday",
|
"pms_url_manual", "week_start_monday",
|
||||||
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
||||||
"notify_consecutive", "notify_upload_posters", "notify_recently_added_upgrade",
|
"notify_consecutive", "notify_upload_posters", "notify_recently_added_upgrade",
|
||||||
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
||||||
@@ -2684,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
|
||||||
|
|
||||||
@@ -2746,7 +2792,7 @@ class WebInterface(object):
|
|||||||
|
|
||||||
# If first run, start websocket
|
# If first run, start websocket
|
||||||
if first_run:
|
if first_run:
|
||||||
web_socket.start_thread()
|
activity_pinger.connect_server(log=True, startup=True)
|
||||||
|
|
||||||
# Reconfigure scheduler if intervals changed
|
# Reconfigure scheduler if intervals changed
|
||||||
if reschedule:
|
if reschedule:
|
||||||
@@ -3505,10 +3551,70 @@ class WebInterface(object):
|
|||||||
return apikey
|
return apikey
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def checkGithub(self, **kwargs):
|
@addtoapi()
|
||||||
|
def update_check(self, **kwargs):
|
||||||
|
""" Check for Tautulli updates.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json
|
||||||
|
{"result": "success",
|
||||||
|
"update": true,
|
||||||
|
"message": "An update for Tautulli is available."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
versioncheck.checkGithub()
|
versioncheck.checkGithub()
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
|
||||||
|
if not plexpy.CURRENT_VERSION:
|
||||||
|
return {'result': 'error',
|
||||||
|
'update': None,
|
||||||
|
'message': 'You are running an unknown version of Tautulli.'
|
||||||
|
}
|
||||||
|
|
||||||
|
elif plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and \
|
||||||
|
plexpy.common.VERSION_NUMBER != plexpy.LATEST_RELEASE:
|
||||||
|
return {'result': 'success',
|
||||||
|
'update': True,
|
||||||
|
'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,
|
||||||
|
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||||
|
'compare_url': helpers.anon_url(
|
||||||
|
'https://github.com/%s/%s/compare/%s...%s'
|
||||||
|
% (plexpy.CONFIG.GIT_USER,
|
||||||
|
plexpy.CONFIG.GIT_REPO,
|
||||||
|
plexpy.CURRENT_VERSION,
|
||||||
|
plexpy.LATEST_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {'result': 'success',
|
||||||
|
'update': False,
|
||||||
|
'message': 'Tautulli is up to date.'
|
||||||
|
}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
|
Reference in New Issue
Block a user