Compare commits
48 Commits
v2.0.13-be
...
v2.0.16-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d15223fb1a | ||
![]() |
d29a12b6db | ||
![]() |
9100e25a21 | ||
![]() |
7672f1955e | ||
![]() |
5f52171fc4 | ||
![]() |
31ac82ad71 | ||
![]() |
38ca4e37a6 | ||
![]() |
3c55550702 | ||
![]() |
7dff6b121b | ||
![]() |
d77d889695 | ||
![]() |
318a21438f | ||
![]() |
7175b57a28 | ||
![]() |
e1e5a050c2 | ||
![]() |
58996c1115 | ||
![]() |
7301fe5f6e | ||
![]() |
a27c423569 | ||
![]() |
19680d3bc7 | ||
![]() |
ecaca4e5dc | ||
![]() |
191de0b577 | ||
![]() |
ebcc073b32 | ||
![]() |
043b3fd57b | ||
![]() |
dd50502dcb | ||
![]() |
f159a1014d | ||
![]() |
abb801535c | ||
![]() |
2732dbf1b1 | ||
![]() |
095d893005 | ||
![]() |
5d8455d141 | ||
![]() |
aa3450bfcc | ||
![]() |
45c2ccdffe | ||
![]() |
fc14c3165f | ||
![]() |
0fad245148 | ||
![]() |
79609c384e | ||
![]() |
09054ddb4b | ||
![]() |
6f912d4aa2 | ||
![]() |
96033a8214 | ||
![]() |
5ca65f4797 | ||
![]() |
d2fccbde68 | ||
![]() |
e6b48d7baf | ||
![]() |
3e51310511 | ||
![]() |
32b43202c2 | ||
![]() |
446170f8de | ||
![]() |
c5a9ecd4ac | ||
![]() |
2af5f817a3 | ||
![]() |
4e55cf3cd4 | ||
![]() |
eeb0478813 | ||
![]() |
33739f1cb2 | ||
![]() |
515e6a8071 | ||
![]() |
2b22f8eb4f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@
|
|||||||
*.db*
|
*.db*
|
||||||
*.db-journal
|
*.db-journal
|
||||||
*.ini
|
*.ini
|
||||||
|
release.lock
|
||||||
version.lock
|
version.lock
|
||||||
logs/*
|
logs/*
|
||||||
cache/*
|
cache/*
|
||||||
|
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,5 +1,47 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.16-beta (2018-01-30)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Timestamp sometimes showing as "0:60" on the activity cards.
|
||||||
|
* Fix: Incorrect session information being shown for playback of synced content.
|
||||||
|
* Fix: Sessions not being stopped when "Playback Stopped" notifications were enabled.
|
||||||
|
* UI:
|
||||||
|
* Fix: Stream resolution showing up as "unknown" on the graphs.
|
||||||
|
* New: Added user filter to the Synced Items table.
|
||||||
|
* Other:
|
||||||
|
* New: Option to use the Plex server update channel when checking for updates.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.15-beta (2018-01-27)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Fix: Live TV sessions not being stopped in History.
|
||||||
|
* Fix: Stream location showing as "unknown" on the activity cards.
|
||||||
|
* New: Improved Live TV details on the activity cards.
|
||||||
|
* Notifications:
|
||||||
|
* New: Added labels and collections to notification parameters.
|
||||||
|
* New: Added more server details to notification parameters.
|
||||||
|
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.14-beta (2018-01-20)
|
||||||
|
|
||||||
|
* Monitoring:
|
||||||
|
* Change: Added "Cellular" bandwidth to "WAN" in activity header.
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Plex Web URL for tracks now go to the album page.
|
||||||
|
* Fix: Recently added notifications being sent for the entire library when DVR EPG data was refreshed.
|
||||||
|
* Fix: Notifier settings not loading with an apostrophe in the custom condition values.
|
||||||
|
* Fix: Custom email addresses not being saved when closing the notifier settings.
|
||||||
|
* Change: Re-enabled Browser notifications.
|
||||||
|
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
|
||||||
|
* Change: Emails no longer automatically insert HTML line breaks.
|
||||||
|
* Change: "Date" header added to email notifications.
|
||||||
|
* UI:
|
||||||
|
* Change: Show all changelogs since the previous version when updating.
|
||||||
|
|
||||||
|
|
||||||
## v2.0.13-beta (2018-01-13)
|
## v2.0.13-beta (2018-01-13)
|
||||||
|
|
||||||
* Notifications:
|
* Notifications:
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Tautulli
|
# Tautulli
|
||||||
|
|
||||||
[](https://discord.gg/36ggawe)
|
[](https://discord.gg/tQcWEUp)
|
||||||
[](https://www.reddit.com/r/Tautulli/)
|
[](https://www.reddit.com/r/Tautulli/)
|
||||||
[](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program)
|
[](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program)
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
|||||||
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
|
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
|
||||||
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
|
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
|
||||||
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
|
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
|
||||||
- For basic questions try asking on [Discord](https://discord.gg/36ggawe), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
|
- For basic questions try asking on [Discord](https://discord.gg/tQcWEUp), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
|
||||||
|
|
||||||
##### If nothing has worked:
|
##### If nothing has worked:
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import version
|
from plexpy import version
|
||||||
from plexpy.helpers import anon_url
|
from plexpy.helpers import anon_url
|
||||||
|
from plexpy.notifiers import BROWSER_NOTIFIERS
|
||||||
%>
|
%>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
||||||
@@ -283,6 +284,9 @@ ${next.modalIncludes()}
|
|||||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||||
<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/jquery.qrcode.min.js"></script>
|
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||||
|
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||||
|
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||||
|
% endif
|
||||||
<script>
|
<script>
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('#updateDismiss').click(function() {
|
$('#updateDismiss').click(function() {
|
||||||
|
@@ -84,7 +84,7 @@ DOCUMENTATION :: END
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Support:</td>
|
<td>Support:</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/36ggawe')}" target="_blank">Tautulli Discord Server</a> |
|
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/tQcWEUp')}" target="_blank">Tautulli Discord Server</a> |
|
||||||
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
|
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
|
||||||
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program')}" target="_blank">Plex Forums</a>
|
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program')}" target="_blank">Plex Forums</a>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -131,19 +131,19 @@ 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 .email {
|
.selectize-control .selectize-input > div .item-value {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .user + .email {
|
.selectize-control .selectize-input > div .item-text + .item-value {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
.selectize-control .selectize-input > div .email:before {
|
.selectize-control .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 .email:after {
|
.selectize-control .selectize-input > div .item-value:after {
|
||||||
content: '>';
|
content: '>';
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -153,6 +153,25 @@ select.form-control:focus,
|
|||||||
display: block;
|
display: block;
|
||||||
color: #a0a0a0;
|
color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
.selectize-control .selectize-dropdown .select-all,
|
||||||
|
.selectize-control .selectize-dropdown .remove-all {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.selectize-control .selectize-dropdown .border-all {
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
margin: 9px -12px 9px -12px;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
.selectize-control .selectize-dropdown .border-all:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup-header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
select.form-control option {
|
select.form-control option {
|
||||||
color: #555;
|
color: #555;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@@ -201,7 +220,7 @@ object {
|
|||||||
}
|
}
|
||||||
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
|
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
|
||||||
background-color: #2f2f2f;
|
background-color: #2f2f2f;
|
||||||
border-color: none;
|
border-color: unset;
|
||||||
}
|
}
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: #282828;
|
background-color: #282828;
|
||||||
@@ -687,8 +706,8 @@ a .users-poster-face:hover {
|
|||||||
height: 290px;
|
height: 290px;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin-right: 20px;
|
margin-right: 25px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
.dashboard-activity-container {
|
.dashboard-activity-container {
|
||||||
height: 240px;
|
height: 240px;
|
||||||
@@ -969,7 +988,6 @@ a .users-poster-face:hover {
|
|||||||
background-image: -o-linear-gradient(top, #fbb450, #f89406);
|
background-image: -o-linear-gradient(top, #fbb450, #f89406);
|
||||||
background-image: linear-gradient(to bottom, #fbb450, #f89406);
|
background-image: linear-gradient(to bottom, #fbb450, #f89406);
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -1106,8 +1124,8 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
|||||||
height: 160px;
|
height: 160px;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin-right: 20px;
|
margin-right: 25px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
.dashboard-stats-container {
|
.dashboard-stats-container {
|
||||||
height: 160px;
|
height: 160px;
|
||||||
@@ -1724,7 +1742,6 @@ a:hover .dashboard-recent-media-cover {
|
|||||||
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||||
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3000000', endColorstr='#e6000000', GradientType=0);
|
|
||||||
webkit-box-shadow: inset 0 0 0 2px #e9a049;
|
webkit-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
box-shadow: inset 0 0 0 2px #e9a049;
|
box-shadow: inset 0 0 0 2px #e9a049;
|
||||||
@@ -1742,6 +1759,18 @@ a:hover .dashboard-recent-media-cover {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .3s;
|
transition: opacity .3s;
|
||||||
}
|
}
|
||||||
|
.summary-poster-face-overlay span:before {
|
||||||
|
content: "View On";
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 34px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
a:hover .summary-poster-face .summary-poster-face-overlay,
|
a:hover .summary-poster-face .summary-poster-face-overlay,
|
||||||
a:hover .summary-poster-face-episode .summary-poster-face-overlay,
|
a:hover .summary-poster-face-episode .summary-poster-face-overlay,
|
||||||
a:hover .summary-poster-face-track .summary-poster-face-overlay,
|
a:hover .summary-poster-face-track .summary-poster-face-overlay,
|
||||||
|
@@ -201,8 +201,8 @@ DOCUMENTATION :: END
|
|||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Container</div>
|
<div class="sub-heading">Container</div>
|
||||||
<div class="sub-value" id="transcode_container-${sk}">
|
<div class="sub-value" id="transcode_container-${sk}">
|
||||||
% if data.get('stream_container_decision') == 'transcode':
|
% if data['stream_container_decision'] == 'transcode':
|
||||||
Transcode (${data['container'].upper()} → ${data['stream_container'].upper()})
|
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${data['container'].upper()})
|
Direct Play (${data['container'].upper()})
|
||||||
% endif
|
% endif
|
||||||
@@ -213,13 +213,13 @@ DOCUMENTATION :: END
|
|||||||
<div class="sub-heading">Video</div>
|
<div class="sub-heading">Video</div>
|
||||||
<div class="sub-value" id="video_decision-${sk}">
|
<div class="sub-value" id="video_decision-${sk}">
|
||||||
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
||||||
% if data.get('stream_video_decision') == 'transcode':
|
% if data['stream_video_decision'] == 'transcode':
|
||||||
<%
|
<%
|
||||||
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
||||||
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
||||||
%>
|
%>
|
||||||
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} → ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||||
% elif data.get('stream_video_decision') == 'copy':
|
% elif data['stream_video_decision'] == 'copy':
|
||||||
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
|
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
|
||||||
@@ -234,9 +234,9 @@ DOCUMENTATION :: END
|
|||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Audio</div>
|
<div class="sub-heading">Audio</div>
|
||||||
<div class="sub-value" id="audio_decision-${sk}">
|
<div class="sub-value" id="audio_decision-${sk}">
|
||||||
% if data.get('stream_audio_decision') == 'transcode':
|
% if data['stream_audio_decision'] == 'transcode':
|
||||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} → ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
% elif data.get('stream_audio_decision') == 'copy':
|
% elif data['stream_audio_decision'] == 'copy':
|
||||||
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
|
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
@@ -250,7 +250,7 @@ DOCUMENTATION :: END
|
|||||||
<div class="sub-value" id="subtitle_decision-${sk}">
|
<div class="sub-value" id="subtitle_decision-${sk}">
|
||||||
% if data['subtitles'] == 1:
|
% if data['subtitles'] == 1:
|
||||||
% if data['stream_subtitle_decision'] == 'transcode':
|
% if data['stream_subtitle_decision'] == 'transcode':
|
||||||
Transcode (${data['subtitle_codec'].upper()} → ${data['stream_subtitle_codec'].upper()})
|
Transcode (${data['subtitle_codec'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()})
|
||||||
% elif data['stream_subtitle_decision'] == 'copy':
|
% elif data['stream_subtitle_decision'] == 'copy':
|
||||||
Direct Stream (${data['subtitle_codec'].upper()})
|
Direct Stream (${data['subtitle_codec'].upper()})
|
||||||
% elif data['stream_subtitle_decision'] == 'burn':
|
% elif data['stream_subtitle_decision'] == 'burn':
|
||||||
@@ -270,7 +270,7 @@ DOCUMENTATION :: END
|
|||||||
<div class="sub-heading">Location</div>
|
<div class="sub-heading">Location</div>
|
||||||
<div class="sub-value time-right">
|
<div class="sub-value time-right">
|
||||||
% if data['ip_address'] != 'N/A':
|
% if data['ip_address'] != 'N/A':
|
||||||
${data['location'].upper()}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
<span id="location-${sk}">${data['location'].upper()}</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>
|
||||||
@@ -312,7 +312,9 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
% if data['media_type'] != 'photo':
|
% if data['media_type'] != 'photo':
|
||||||
<div class="dashboard-activity-info-time">
|
<div class="dashboard-activity-info-time">
|
||||||
% if data['view_offset']:
|
% if data['live'] == 1:
|
||||||
|
<br />Live
|
||||||
|
% elif data['view_offset']:
|
||||||
ETA:
|
ETA:
|
||||||
<span id="stream-eta-${sk}">
|
<span id="stream-eta-${sk}">
|
||||||
<script>
|
<script>
|
||||||
@@ -340,8 +342,12 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-progress">
|
<div class="dashboard-activity-progress">
|
||||||
<div class="dashboard-activity-progress-bar">
|
<div class="dashboard-activity-progress-bar">
|
||||||
|
% if data['live'] == 1:
|
||||||
|
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-toggle="tooltip" title="Stream Progress Live">Live</div>
|
||||||
|
% else:
|
||||||
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
|
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
|
||||||
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
|
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,7 +395,11 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-subtitle-container">
|
<div class="dashboard-activity-metadata-subtitle-container">
|
||||||
% if data['channel_stream'] == 0:
|
% if data['live'] == 1:
|
||||||
|
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Plex Live TV">
|
||||||
|
<i class="fa fa-fw fa-television"></i>
|
||||||
|
</div>
|
||||||
|
% elif data['channel_stream'] == 0:
|
||||||
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
|
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
|
||||||
% if data['media_type'] == 'movie':
|
% if data['media_type'] == 'movie':
|
||||||
<i class="fa fa-fw fa-film"></i>
|
<i class="fa fa-fw fa-film"></i>
|
||||||
@@ -404,12 +414,14 @@ DOCUMENTATION :: END
|
|||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div id="media-type-${sk}" title="Channel">
|
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Channel">
|
||||||
<i class="fa fa-fw fa-cloud"></i>
|
<i class="fa fa-fw fa-cloud"></i>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
<div class="dashboard-activity-metadata-subtitle">
|
<div class="dashboard-activity-metadata-subtitle">
|
||||||
% if data['channel_stream'] == 0:
|
% if data['live'] == 1:
|
||||||
|
<span title="Plex Live TV" class="sub-heading">Plex Live TV</span>
|
||||||
|
% elif data['channel_stream'] == 0:
|
||||||
% 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':
|
||||||
|
@@ -114,7 +114,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_user_names',
|
url: 'get_user_names',
|
||||||
type: 'get',
|
type: 'get',
|
||||||
dataType: "json",
|
dataType: 'json',
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
var select = $('#history-user');
|
var select = $('#history-user');
|
||||||
data.sort(function (a, b) {
|
data.sort(function (a, b) {
|
||||||
@@ -130,7 +130,6 @@
|
|||||||
function loadHistoryTable(media_type, selected_user_id) {
|
function loadHistoryTable(media_type, selected_user_id) {
|
||||||
history_table_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),
|
||||||
@@ -138,9 +137,13 @@
|
|||||||
user_id: selected_user_id
|
user_id: selected_user_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
history_table = $('#history_table').DataTable(history_table_options);
|
history_table = $('#history_table').DataTable(history_table_options);
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<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]
|
||||||
|
});
|
||||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
clearSearchButton('history_table', history_table);
|
clearSearchButton('history_table', history_table);
|
||||||
@@ -160,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var media_type = null;
|
var media_type = null;
|
||||||
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}";
|
||||||
loadHistoryTable(media_type, selected_user_id);
|
loadHistoryTable(media_type, selected_user_id);
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
|
@@ -131,12 +131,13 @@
|
|||||||
<%def name="modalIncludes()">
|
<%def name="modalIncludes()">
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
|
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
|
||||||
|
<% from plexpy.common import VERSION_NUMBER %>
|
||||||
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
|
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<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">Tautulli Updated</h4>
|
<h4 class="modal-title">Tautulli Updated to <strong>${VERSION_NUMBER}</strong></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
</div>
|
</div>
|
||||||
@@ -308,20 +309,23 @@
|
|||||||
streams_header = streams_header.replace(/, $/, '') + ')';
|
streams_header = streams_header.replace(/, $/, '') + ')';
|
||||||
$('#currentActivityHeader-streams').text(streams_header);
|
$('#currentActivityHeader-streams').text(streams_header);
|
||||||
|
|
||||||
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps')) + ' (';
|
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps'));
|
||||||
|
var lan_wan_bandwidth_header = '';
|
||||||
if (lan_bw) {
|
if (lan_bw) {
|
||||||
bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
||||||
}
|
}
|
||||||
if (wan_bw) {
|
if (wan_bw) {
|
||||||
bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
||||||
|
}
|
||||||
|
if (lan_wan_bandwidth_header) {
|
||||||
|
bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')';
|
||||||
}
|
}
|
||||||
bandwidth_header = bandwidth_header.replace(/, $/, '') + ')';
|
|
||||||
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
||||||
|
|
||||||
$('#currentActivityHeader').show();
|
$('#currentActivityHeader').show();
|
||||||
|
|
||||||
sessions.forEach(function (session) {
|
sessions.forEach(function (session) {
|
||||||
var s = new Proxy(session, defaultHandler);
|
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
|
||||||
var key = s.session_key;
|
var key = s.session_key;
|
||||||
var session_id = s.session_id;
|
var session_id = s.session_id;
|
||||||
var instance = $('#activity-instance-' + key);
|
var instance = $('#activity-instance-' + key);
|
||||||
@@ -395,7 +399,7 @@
|
|||||||
|
|
||||||
var transcode_container = '';
|
var transcode_container = '';
|
||||||
if (s.stream_container_decision === 'transcode') {
|
if (s.stream_container_decision === 'transcode') {
|
||||||
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' → ' + s.stream_container.toUpperCase() + ')';
|
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
|
||||||
} else {
|
} else {
|
||||||
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
|
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
|
||||||
}
|
}
|
||||||
@@ -428,7 +432,7 @@
|
|||||||
if (s.stream_video_decision === 'transcode') {
|
if (s.stream_video_decision === 'transcode') {
|
||||||
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
||||||
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
||||||
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' → ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
||||||
} else if (s.stream_video_decision === 'copy') {
|
} else if (s.stream_video_decision === 'copy') {
|
||||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
||||||
} else {
|
} else {
|
||||||
@@ -444,7 +448,7 @@
|
|||||||
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
||||||
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
||||||
if (s.stream_audio_decision === 'transcode') {
|
if (s.stream_audio_decision === 'transcode') {
|
||||||
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' → ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' <i class="fa fa-long-arrow-right"></i> ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||||
} else if (s.stream_audio_decision === 'copy') {
|
} else if (s.stream_audio_decision === 'copy') {
|
||||||
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||||
} else {
|
} else {
|
||||||
@@ -456,7 +460,7 @@
|
|||||||
var subtitle_decision = 'None';
|
var subtitle_decision = 'None';
|
||||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
|
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
|
||||||
if (s.stream_subtitle_decision === 'transcode') {
|
if (s.stream_subtitle_decision === 'transcode') {
|
||||||
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' → ' + s.stream_subtitle_codec.toUpperCase() + ')';
|
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')';
|
||||||
} else if (s.stream_subtitle_decision === 'copy') {
|
} else if (s.stream_subtitle_decision === 'copy') {
|
||||||
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
|
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
|
||||||
} else if (s.stream_subtitle_decision === 'burn') {
|
} else if (s.stream_subtitle_decision === 'burn') {
|
||||||
@@ -484,6 +488,8 @@
|
|||||||
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
|
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
|
||||||
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
|
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
|
||||||
|
|
||||||
|
$('#location-' + key).html(s.location.toUpperCase());
|
||||||
|
|
||||||
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
|
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
|
||||||
var bw = parseInt(s.bandwidth);
|
var bw = parseInt(s.bandwidth);
|
||||||
if (bw !== "Unknown") {
|
if (bw !== "Unknown") {
|
||||||
@@ -509,7 +515,7 @@
|
|||||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||||
var progress_bar = $('#progress-bar-' + key);
|
var progress_bar = $('#progress-bar-' + key);
|
||||||
progress_bar.data('state', s.state);
|
progress_bar.data('state', s.state);
|
||||||
if (progress_bar.data('last_view_offset') !== s.view_offset) {
|
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
|
||||||
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,7 +823,7 @@
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_changelog',
|
url: 'get_changelog',
|
||||||
data: {
|
data: {
|
||||||
latest_only: true,
|
since_prev_release: true,
|
||||||
update_shown: true
|
update_shown: true
|
||||||
},
|
},
|
||||||
cache: false,
|
cache: false,
|
||||||
|
@@ -117,9 +117,9 @@ DOCUMENTATION :: END
|
|||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||||
% if data['media_type'] == 'track':
|
% if data['media_type'] == 'track':
|
||||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
|
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
|
||||||
% else:
|
% else:
|
||||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
|
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
|
||||||
% endif
|
% endif
|
||||||
% if data['media_type'] == 'episode':
|
% if data['media_type'] == 'episode':
|
||||||
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
|
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
PNotify.prototype.options.addclass = "stack-bottomright";
|
PNotify.prototype.options.addclass = "stack-bottomright";
|
||||||
PNotify.prototype.options.buttons.closer_hover = false;
|
PNotify.prototype.options.buttons.closer_hover = false;
|
||||||
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo.png' }
|
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo-circle.png' };
|
||||||
PNotify.prototype.options.history = false;
|
PNotify.prototype.options.history = false;
|
||||||
PNotify.prototype.options.shadow = false;
|
PNotify.prototype.options.shadow = false;
|
||||||
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
|
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
|
||||||
@@ -21,7 +21,7 @@ function check_notifications() {
|
|||||||
$.getJSON('get_browser_notifications', function (data) {
|
$.getJSON('get_browser_notifications', function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
$.each(data, function (i, notification) {
|
$.each(data, function (i, notification) {
|
||||||
if (notification.delay == 0) {
|
if (notification.delay === 0) {
|
||||||
PNotify.prototype.options.hide = false;
|
PNotify.prototype.options.hide = false;
|
||||||
} else {
|
} else {
|
||||||
PNotify.prototype.options.hide = true;
|
PNotify.prototype.options.hide = true;
|
||||||
@@ -34,7 +34,7 @@ function check_notifications() {
|
|||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
check_notifications();
|
check_notifications();
|
||||||
}, 3000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
@@ -290,19 +290,9 @@ String.prototype.toProperCase = function () {
|
|||||||
|
|
||||||
function millisecondsToMinutes(ms, roundToMinute) {
|
function millisecondsToMinutes(ms, roundToMinute) {
|
||||||
if (ms > 0) {
|
if (ms > 0) {
|
||||||
seconds = ms / 1000;
|
var minutes = Math.floor(ms / 60000);
|
||||||
minutes = seconds / 60;
|
var seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||||
if (roundToMinute) {
|
return (seconds == 60 ? (minutes+1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
|
||||||
output = Math.round(minutes, 0)
|
|
||||||
} else {
|
|
||||||
minutesFloor = Math.floor(minutes);
|
|
||||||
secondsReal = Math.round((seconds - (minutesFloor * 60)), 0);
|
|
||||||
if (secondsReal < 10) {
|
|
||||||
secondsReal = '0' + secondsReal;
|
|
||||||
}
|
|
||||||
output = minutesFloor + ':' + secondsReal;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
} else {
|
} else {
|
||||||
if (roundToMinute) {
|
if (roundToMinute) {
|
||||||
return '0';
|
return '0';
|
||||||
|
@@ -21,7 +21,7 @@ history_table_options = {
|
|||||||
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||||
"emptyTable": "No data in table",
|
"emptyTable": "No data in table",
|
||||||
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
||||||
},
|
},
|
||||||
"pagingType": "full_numbers",
|
"pagingType": "full_numbers",
|
||||||
"stateSave": true,
|
"stateSave": true,
|
||||||
"processing": false,
|
"processing": false,
|
||||||
@@ -172,7 +172,7 @@ history_table_options = {
|
|||||||
},
|
},
|
||||||
"width": "33%",
|
"width": "33%",
|
||||||
"className": "datatable-wrap"
|
"className": "datatable-wrap"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [7],
|
"targets": [7],
|
||||||
"data":"started",
|
"data":"started",
|
||||||
@@ -322,7 +322,7 @@ history_table_options = {
|
|||||||
$(row).addClass('current-activity-row');
|
$(row).addClass('current-activity-row');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Parent table platform modal
|
// Parent table platform modal
|
||||||
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||||
|
@@ -98,7 +98,7 @@ sync_table_options = {
|
|||||||
"data": "total_size",
|
"data": "total_size",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData > 0 ) {
|
if (cellData > 0 ) {
|
||||||
megabytes = Math.round((cellData/1024)/1024, 0)
|
megabytes = Math.round((cellData/1024)/1024, 0);
|
||||||
$(td).html(megabytes + 'MB');
|
$(td).html(megabytes + 'MB');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('0MB');
|
$(td).html('0MB');
|
||||||
@@ -144,14 +144,16 @@ sync_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)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$('#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();
|
||||||
|
|
||||||
var index_delete = syncs_to_delete.findIndex(x => x.client_id == rowData['client_id'] && x.sync_id == rowData['sync_id']);
|
var index_delete = syncs_to_delete.findIndex(function (x) {
|
||||||
|
return x.client_id === rowData['client_id'] && x.sync_id === rowData['sync_id'];
|
||||||
|
});
|
||||||
|
|
||||||
if (index_delete === -1) {
|
if (index_delete === -1) {
|
||||||
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });
|
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });
|
||||||
|
@@ -385,8 +385,9 @@
|
|||||||
|
|
||||||
$("#clear-logs").click(function () {
|
$("#clear-logs").click(function () {
|
||||||
var logfile = $(".tab-pane.active").data('logfile')
|
var logfile = $(".tab-pane.active").data('logfile')
|
||||||
|
var title = $("#log_tabs li.active a").text()
|
||||||
|
|
||||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli logs?");
|
$("#confirm-message").text("Are you sure you want to clear the " + title + "?");
|
||||||
$('#confirm-modal').modal();
|
$('#confirm-modal').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -421,7 +422,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-notify-logs").click(function () {
|
$("#clear-notify-logs").click(function () {
|
||||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli notification logs?");
|
$("#confirm-message").text("Are you sure you want to clear the Tautulli Notification Logs?");
|
||||||
$('#confirm-modal').modal();
|
$('#confirm-modal').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -442,7 +443,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-login-logs").click(function () {
|
$("#clear-login-logs").click(function () {
|
||||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli login logs?");
|
$("#confirm-message").text("Are you sure you want to clear the Tautulli Login Logs?");
|
||||||
$('#confirm-modal').modal();
|
$('#confirm-modal').modal();
|
||||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@@ -98,6 +98,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="help-block">${item['description'] | n}</p>
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
</div>
|
</div>
|
||||||
|
% elif item['input_type'] == 'selectize':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||||
|
<option value="select-all">Select All</option>
|
||||||
|
<option value="remove-all">Remove All</option>
|
||||||
|
% if isinstance(item['select_options'], dict):
|
||||||
|
% for section, options in item['select_options'].iteritems():
|
||||||
|
<optgroup label="${section}">
|
||||||
|
% for option in sorted(options, key=lambda x: x['text'].lower()):
|
||||||
|
<option value="${option['value']}">${option['text']}</option>
|
||||||
|
% endfor
|
||||||
|
</optgroup>
|
||||||
|
% endfor
|
||||||
|
% else:
|
||||||
|
<option value="border-all"></option>
|
||||||
|
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
|
||||||
|
<option value="${option['value']}">${option['text']}</option>
|
||||||
|
% endfor
|
||||||
|
% endif
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description'] | n}</p>
|
||||||
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
</div>
|
</div>
|
||||||
@@ -306,7 +333,7 @@
|
|||||||
$('#notifier-config-modal').unbind('hidden.bs.modal');
|
$('#notifier-config-modal').unbind('hidden.bs.modal');
|
||||||
|
|
||||||
// Need this for setting conditions since conditions contain the character "
|
// Need this for setting conditions since conditions contain the character "
|
||||||
$('#custom_conditions').val('${notifier['custom_conditions'] | n}')
|
$('#custom_conditions').val(${json.dumps(notifier["custom_conditions"]) | n});
|
||||||
|
|
||||||
$('#condition-widget').filterer({
|
$('#condition-widget').filterer({
|
||||||
parameters: ${parameters | n},
|
parameters: ${parameters | n},
|
||||||
@@ -314,7 +341,7 @@
|
|||||||
updateConditions: function(newConditions){
|
updateConditions: function(newConditions){
|
||||||
$('#custom_conditions').val(JSON.stringify(newConditions));
|
$('#custom_conditions').val(JSON.stringify(newConditions));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
function reloadModal() {
|
function reloadModal() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -332,7 +359,7 @@
|
|||||||
if (jqXHR) {
|
if (jqXHR) {
|
||||||
var result = $.parseJSON(jqXHR.responseText);
|
var result = $.parseJSON(jqXHR.responseText);
|
||||||
var msg = result.message;
|
var msg = result.message;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||||
} else {
|
} else {
|
||||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||||
@@ -392,7 +419,7 @@
|
|||||||
|
|
||||||
% if notifier['agent_name'] == 'facebook':
|
% if notifier['agent_name'] == 'facebook':
|
||||||
function disableFacebookRequest() {
|
function disableFacebookRequest() {
|
||||||
if ($('#facebook_app_id').val() != '' && $('#facebook_app_secret').val() != '') { $('#facebook_facebookStep1').prop('disabled', false); }
|
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebookStep1').prop('disabled', false); }
|
||||||
else { $('#facebook_facebookStep1').prop('disabled', true); }
|
else { $('#facebook_facebookStep1').prop('disabled', true); }
|
||||||
}
|
}
|
||||||
disableFacebookRequest();
|
disableFacebookRequest();
|
||||||
@@ -406,19 +433,20 @@
|
|||||||
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
|
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var facebook_token;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'facebookStep1',
|
url: 'facebookStep1',
|
||||||
data: {
|
data: {
|
||||||
app_id: $('#facebook_app_id').val(),
|
app_id: $('#facebook_app_id').val(),
|
||||||
app_secret: $('#facebook_app_secret').val(),
|
app_secret: $('#facebook_app_secret').val(),
|
||||||
redirect_uri: $('#facebook_redirect_uri').val(),
|
redirect_uri: $('#facebook_redirect_uri').val()
|
||||||
},
|
},
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function (xhr, status) {
|
complete: function (xhr, status) {
|
||||||
var result = $.parseJSON(xhr.responseText);
|
var result = $.parseJSON(xhr.responseText);
|
||||||
var msg = result.msg;
|
var msg = result.msg;
|
||||||
if (result.result == 'success') {
|
if (result.result === 'success') {
|
||||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||||
window.open(result.url);
|
window.open(result.url);
|
||||||
|
|
||||||
@@ -457,18 +485,18 @@
|
|||||||
|
|
||||||
$('#notifier-config-modal').on('hidden.bs.modal', function () {
|
$('#notifier-config-modal').on('hidden.bs.modal', function () {
|
||||||
facebook_token = false;
|
facebook_token = false;
|
||||||
})
|
});
|
||||||
|
|
||||||
% elif notifier['agent_name'] == 'browser':
|
% elif notifier['agent_name'] == 'browser':
|
||||||
$('#browser_allow_browser').click(function () {
|
$('#browser_allow_browser').click(function () {
|
||||||
PNotify.desktop.permission();
|
PNotify.desktop.permission();
|
||||||
})
|
});
|
||||||
|
|
||||||
% elif notifier['agent_name'] == 'osx':
|
% elif notifier['agent_name'] == 'osx':
|
||||||
$('#osxnotifyregister').click(function () {
|
$('#osxnotifyregister').click(function () {
|
||||||
var osx_notify_app = $('#osx_notify_app').val();
|
var osx_notify_app = $('#osx_notify_app').val();
|
||||||
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||||
})
|
});
|
||||||
|
|
||||||
% elif notifier['agent_name'] == 'email':
|
% elif notifier['agent_name'] == 'email':
|
||||||
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
|
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
|
||||||
@@ -477,26 +505,37 @@
|
|||||||
plugins: ['remove_button'],
|
plugins: ['remove_button'],
|
||||||
persist: false,
|
persist: false,
|
||||||
maxItems: null,
|
maxItems: null,
|
||||||
valueField: 'email',
|
|
||||||
labelField: 'user',
|
|
||||||
searchField: ['user', 'email'],
|
|
||||||
options: ${json.dumps(user_emails) | n},
|
|
||||||
render: {
|
render: {
|
||||||
item: function(item, escape) {
|
item: function(item, escape) {
|
||||||
return '<div>' +
|
return '<div>' +
|
||||||
(item.user ? '<span class="user">' + escape(item.user) + '</span>' : '') +
|
(item.text ? '<span class="item-text">' + escape(item.text) + '</span>' : '') +
|
||||||
(item.email ? '<span class="email">' + escape(item.email) + '</span>' : '') +
|
(item.value ? '<span class="item-value">' + escape(item.value) + '</span>' : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
},
|
},
|
||||||
option: function(item, escape) {
|
option: function(item, escape) {
|
||||||
var label = item.user || item.email;
|
var label = item.text || item.value;
|
||||||
var caption = item.user ? item.email : null;
|
var caption = item.text ? item.value : null;
|
||||||
|
if (item.value.endsWith('-all')) {
|
||||||
|
return '<div class="' + item.value + '">' + escape(label) + '</div>'
|
||||||
|
}
|
||||||
return '<div>' +
|
return '<div>' +
|
||||||
escape(label) +
|
escape(label) +
|
||||||
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onItemAdd: function(value) {
|
||||||
|
if (value === 'select-all') {
|
||||||
|
var all_keys = $.map(this.options, function(option){
|
||||||
|
return option.value.endsWith('-all') ? null : option.value;
|
||||||
|
});
|
||||||
|
this.setValue(all_keys);
|
||||||
|
} else if (value === 'remove-all') {
|
||||||
|
this.clear();
|
||||||
|
this.refreshOptions();
|
||||||
|
this.positionDropdown();
|
||||||
|
}
|
||||||
|
},
|
||||||
createFilter: function(input) {
|
createFilter: function(input) {
|
||||||
var match, regex;
|
var match, regex;
|
||||||
|
|
||||||
@@ -514,16 +553,15 @@
|
|||||||
},
|
},
|
||||||
create: function(input) {
|
create: function(input) {
|
||||||
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
|
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
|
||||||
return {email: input};
|
return {value: input};
|
||||||
}
|
}
|
||||||
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
|
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
|
||||||
if (match) {
|
if (match) {
|
||||||
return {
|
return {
|
||||||
email : match[2],
|
value : match[2],
|
||||||
user : $.trim(match[1])
|
text : $.trim(match[1])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
alert('Invalid email address.');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -541,7 +579,6 @@
|
|||||||
create: true
|
create: true
|
||||||
});
|
});
|
||||||
var join_device_names = $join_device_names[0].selectize;
|
var join_device_names = $join_device_names[0].selectize;
|
||||||
console.log(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
|
|
||||||
join_device_names.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
|
join_device_names.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
@@ -673,7 +710,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function sendTestNotification() {
|
function sendTestNotification() {
|
||||||
if ('${notifier["agent_name"]}' != 'browser') {
|
if ('${notifier["agent_name"]}' !== 'browser') {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'send_notification',
|
url: 'send_notification',
|
||||||
data: {
|
data: {
|
||||||
@@ -697,7 +734,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if ($('#browser_auto_hide_delay').val() == "0") {
|
if ($('#browser_auto_hide_delay').val() === "0") {
|
||||||
PNotify.prototype.options.hide = false;
|
PNotify.prototype.options.hide = false;
|
||||||
} else {
|
} else {
|
||||||
PNotify.prototype.options.hide = true;
|
PNotify.prototype.options.hide = true;
|
||||||
|
@@ -42,7 +42,7 @@ DOCUMENTATION :: END
|
|||||||
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
|
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
|
||||||
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
% elif job in ('Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
|
% elif job in ('Check for server response', 'Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
|
||||||
<tr>
|
<tr>
|
||||||
<td>${job}</td>
|
<td>${job}</td>
|
||||||
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
|
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
|
||||||
|
@@ -553,9 +553,10 @@
|
|||||||
<div id="pms_update_options">
|
<div id="pms_update_options">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-3">
|
||||||
<label for="pms_update_channel">Update Channel</label>
|
<label for="pms_update_channel">Update Channel</label>
|
||||||
<select class="form-control" id="pms_update_channel" name="pms_update_channel">
|
<select class="form-control" id="pms_update_channel" name="pms_update_channel">
|
||||||
|
<option value="plex">Use Server Setting</option>
|
||||||
<option value="public">Public</option>
|
<option value="public">Public</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -2104,32 +2105,41 @@ $(document).ready(function() {
|
|||||||
var update_channel = update_params.pms_update_channel;
|
var update_channel = update_params.pms_update_channel;
|
||||||
var update_distro = update_params.pms_update_distro;
|
var update_distro = update_params.pms_update_distro;
|
||||||
var update_distro_build = update_params.pms_update_distro_build;
|
var update_distro_build = update_params.pms_update_distro_build;
|
||||||
|
var plex_update_channel = update_params.plex_update_channel;
|
||||||
|
|
||||||
$("#pms_update_channel option[value='plexpass']").remove();
|
$('#pms_update_channel option[value=beta]').remove();
|
||||||
if (plexpass) {
|
if (plexpass) {
|
||||||
var selected = (update_channel == 'plexpass') ? true : false;
|
var selected = (update_channel == 'beta') ? true : false;
|
||||||
$('#pms_update_channel')
|
$('#pms_update_channel')
|
||||||
.append($('<option></option>')
|
.append($('<option></option>')
|
||||||
.text('Plex Pass')
|
.text('Beta')
|
||||||
.val('plexpass')
|
.val('beta')
|
||||||
.prop('selected', selected));
|
.prop('selected', selected));
|
||||||
}
|
}
|
||||||
|
|
||||||
$.getJSON('https://plex.tv/api/downloads/1.json?channel=' + update_channel, function (downloads) {
|
$.ajax({
|
||||||
platform_downloads = downloads.computer[platform] || downloads.nas[platform];
|
url: 'https://plex.tv/api/downloads/1.json?channel=' + plex_update_channel,
|
||||||
if (platform_downloads) {
|
type: 'GET',
|
||||||
$("#pms_update_distro_build option").remove();
|
dataType: 'json',
|
||||||
$.each(platform_downloads.releases, function (index, item) {
|
beforeSend: function (xhr) {
|
||||||
var label = (platform_downloads.releases.length == 1) ? platform_downloads.name : platform_downloads.name + ' - ' + item.label;
|
xhr.setRequestHeader('X-Plex-Token', $('#pms_token').val());
|
||||||
var selected = (item.distro == update_distro && item.build == update_distro_build) ? true : false;
|
},
|
||||||
$('#pms_update_distro_build')
|
success: function (downloads) {
|
||||||
.append($('<option></option>')
|
var platform_downloads = downloads.computer[platform] || downloads.nas[platform];
|
||||||
.text(label)
|
if (platform_downloads) {
|
||||||
.val(item.build)
|
$("#pms_update_distro_build option").remove();
|
||||||
.attr('data-distro', item.distro)
|
$.each(platform_downloads.releases, function (index, item) {
|
||||||
.prop('selected', selected));
|
var label = (platform_downloads.releases.length === 1) ? platform_downloads.name : platform_downloads.name + ' - ' + item.label;
|
||||||
})
|
var selected = (item.distro === update_distro && item.build === update_distro_build) ? true : false;
|
||||||
$('#pms_update_distro').val($("#pms_update_distro_build option:selected").data('distro'))
|
$('#pms_update_distro_build')
|
||||||
|
.append($('<option></option>')
|
||||||
|
.text(label)
|
||||||
|
.val(item.build)
|
||||||
|
.attr('data-distro', item.distro)
|
||||||
|
.prop('selected', selected));
|
||||||
|
});
|
||||||
|
$('#pms_update_distro').val($('#pms_update_distro_build option:selected').data('distro'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -27,6 +27,16 @@
|
|||||||
</button> 
|
</button> 
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
|
<div class="btn-group" id="user-selection">
|
||||||
|
<label>
|
||||||
|
<select name="sync-user" id="sync-user" class="btn" style="color: inherit;">
|
||||||
|
<option value="">All Users</option>
|
||||||
|
<option disabled>────────────</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<div class="btn-group">
|
<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>
|
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,17 +97,45 @@
|
|||||||
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
|
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
sync_table_options.ajax = {
|
// Load user ids and names (for the selector)
|
||||||
url: 'get_sync',
|
$.ajax({
|
||||||
data: function (d) {
|
url: 'get_user_names',
|
||||||
d.user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
type: 'get',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (data) {
|
||||||
|
var select = $('#sync-user');
|
||||||
|
data.sort(function (a, b) {
|
||||||
|
return a.friendly_name.localeCompare(b.friendly_name);
|
||||||
|
});
|
||||||
|
data.forEach(function (item) {
|
||||||
|
select.append('<option value="' + item.user_id + '">' +
|
||||||
|
item.friendly_name + '</option>');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
|
||||||
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0] } );
|
|
||||||
$( colvis.button() ).appendTo('div.colvis-button-bar');
|
|
||||||
|
|
||||||
clearSearchButton('sync_table', sync_table);
|
function loadSyncTable(selected_user_id) {
|
||||||
|
sync_table_options.ajax = {
|
||||||
|
url: 'get_sync?user_id=' + selected_user_id
|
||||||
|
};
|
||||||
|
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||||
|
var colvis = new $.fn.dataTable.ColVis(sync_table, {
|
||||||
|
buttonText: '<i class="fa fa-columns"></i> Select columns',
|
||||||
|
buttonClass: 'btn btn-dark',
|
||||||
|
exclude: [0]
|
||||||
|
});
|
||||||
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
|
clearSearchButton('sync_table', sync_table);
|
||||||
|
|
||||||
|
$('#sync-user').on('change', function () {
|
||||||
|
selected_user_id = $(this).val() || null;
|
||||||
|
sync_table.ajax.url('get_sync?user_id=' + selected_user_id).load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}";
|
||||||
|
loadSyncTable(selected_user_id);
|
||||||
|
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
$('#row-edit-mode').on('click', function() {
|
$('#row-edit-mode').on('click', function() {
|
||||||
|
@@ -34,7 +34,7 @@ from apscheduler.triggers.interval import IntervalTrigger
|
|||||||
|
|
||||||
import activity_handler
|
import activity_handler
|
||||||
import activity_pinger
|
import activity_pinger
|
||||||
import config
|
import common
|
||||||
import database
|
import database
|
||||||
import libraries
|
import libraries
|
||||||
import logger
|
import logger
|
||||||
@@ -42,7 +42,6 @@ import mobile_app
|
|||||||
import notification_handler
|
import notification_handler
|
||||||
import notifiers
|
import notifiers
|
||||||
import plextv
|
import plextv
|
||||||
import pmsconnect
|
|
||||||
import users
|
import users
|
||||||
import versioncheck
|
import versioncheck
|
||||||
import plexpy.config
|
import plexpy.config
|
||||||
@@ -83,6 +82,7 @@ INSTALL_TYPE = None
|
|||||||
CURRENT_VERSION = None
|
CURRENT_VERSION = None
|
||||||
LATEST_VERSION = None
|
LATEST_VERSION = None
|
||||||
COMMITS_BEHIND = None
|
COMMITS_BEHIND = None
|
||||||
|
PREV_RELEASE = None
|
||||||
|
|
||||||
UMASK = None
|
UMASK = None
|
||||||
|
|
||||||
@@ -102,7 +102,9 @@ def initialize(config_file):
|
|||||||
global _INITIALIZED
|
global _INITIALIZED
|
||||||
global CURRENT_VERSION
|
global CURRENT_VERSION
|
||||||
global LATEST_VERSION
|
global LATEST_VERSION
|
||||||
|
global PREV_RELEASE
|
||||||
global UMASK
|
global UMASK
|
||||||
|
|
||||||
CONFIG = plexpy.config.Config(config_file)
|
CONFIG = plexpy.config.Config(config_file)
|
||||||
CONFIG_FILE = config_file
|
CONFIG_FILE = config_file
|
||||||
|
|
||||||
@@ -190,6 +192,17 @@ def initialize(config_file):
|
|||||||
CONFIG.JWT_SECRET = generate_uuid()
|
CONFIG.JWT_SECRET = generate_uuid()
|
||||||
CONFIG.write()
|
CONFIG.write()
|
||||||
|
|
||||||
|
# Get the previous version from the file
|
||||||
|
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
||||||
|
prev_version = None
|
||||||
|
if os.path.isfile(version_lock_file):
|
||||||
|
try:
|
||||||
|
with open(version_lock_file, "r") as fp:
|
||||||
|
prev_version = fp.read()
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"Unable to read previous version from file '%s': %s" %
|
||||||
|
(version_lock_file, e))
|
||||||
|
|
||||||
# Get the currently installed version. Returns None, 'win32' or the git
|
# Get the currently installed version. Returns None, 'win32' or the git
|
||||||
# hash.
|
# hash.
|
||||||
CURRENT_VERSION, CONFIG.GIT_REMOTE, CONFIG.GIT_BRANCH = versioncheck.getVersion()
|
CURRENT_VERSION, CONFIG.GIT_REMOTE, CONFIG.GIT_BRANCH = versioncheck.getVersion()
|
||||||
@@ -198,8 +211,6 @@ def initialize(config_file):
|
|||||||
# This allowes one to restore to that version. The idea is that if we
|
# This allowes one to restore to that version. The idea is that if we
|
||||||
# arrive here, most parts of Tautulli seem to work.
|
# arrive here, most parts of Tautulli seem to work.
|
||||||
if CURRENT_VERSION:
|
if CURRENT_VERSION:
|
||||||
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(version_lock_file, "w") as fp:
|
with open(version_lock_file, "w") as fp:
|
||||||
fp.write(CURRENT_VERSION)
|
fp.write(CURRENT_VERSION)
|
||||||
@@ -217,6 +228,32 @@ def initialize(config_file):
|
|||||||
else:
|
else:
|
||||||
LATEST_VERSION = CURRENT_VERSION
|
LATEST_VERSION = CURRENT_VERSION
|
||||||
|
|
||||||
|
# Get the previous release from the file
|
||||||
|
release_file = os.path.join(DATA_DIR, "release.lock")
|
||||||
|
PREV_RELEASE = common.VERSION_NUMBER
|
||||||
|
if os.path.isfile(release_file):
|
||||||
|
try:
|
||||||
|
with open(release_file, "r") as fp:
|
||||||
|
PREV_RELEASE = fp.read()
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"Unable to read previous release from file '%s': %s" %
|
||||||
|
(release_file, e))
|
||||||
|
elif prev_version == 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca': # Commit hash for v1.4.25
|
||||||
|
PREV_RELEASE = 'v1.4.25'
|
||||||
|
|
||||||
|
# Check if the release was updated
|
||||||
|
if common.VERSION_NUMBER != PREV_RELEASE:
|
||||||
|
CONFIG.UPDATE_SHOW_CHANGELOG = 1
|
||||||
|
CONFIG.write()
|
||||||
|
|
||||||
|
# Write current release version to file for update checking
|
||||||
|
try:
|
||||||
|
with open(release_file, "w") as fp:
|
||||||
|
fp.write(common.VERSION_NUMBER)
|
||||||
|
except IOError as e:
|
||||||
|
logger.error(u"Unable to write current release to file '%s': %s" %
|
||||||
|
(release_file, e))
|
||||||
|
|
||||||
# Get the real PMS urls for SSL and remote access
|
# Get the real PMS urls for SSL and remote access
|
||||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||||
plextv.get_server_resources()
|
plextv.get_server_resources()
|
||||||
@@ -345,7 +382,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 server response',
|
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -367,7 +404,7 @@ def initialize_scheduler():
|
|||||||
response_seconds = CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS * CONFIG.WEBSOCKET_CONNECTION_TIMEOUT
|
response_seconds = CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS * CONFIG.WEBSOCKET_CONNECTION_TIMEOUT
|
||||||
response_seconds = 60 if response_seconds < 60 else response_seconds
|
response_seconds = 60 if response_seconds < 60 else response_seconds
|
||||||
|
|
||||||
schedule_job(activity_pinger.check_server_response, 'Check server response',
|
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=response_seconds)
|
hours=0, minutes=0, seconds=response_seconds)
|
||||||
|
|
||||||
# Start scheduler
|
# Start scheduler
|
||||||
@@ -410,6 +447,7 @@ def start():
|
|||||||
|
|
||||||
# Start background notification thread
|
# Start background notification thread
|
||||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||||
|
notifiers.check_browser_enabled()
|
||||||
|
|
||||||
_STARTED = True
|
_STARTED = True
|
||||||
|
|
||||||
@@ -1549,6 +1587,7 @@ def upgrade():
|
|||||||
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)
|
||||||
|
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
|
||||||
|
|
||||||
# Stop the notification threads
|
# Stop the notification threads
|
||||||
for i in range(CONFIG.NOTIFICATION_THREADS):
|
for i in range(CONFIG.NOTIFICATION_THREADS):
|
||||||
@@ -1579,23 +1618,35 @@ def shutdown(restart=False, update=False, checkout=False):
|
|||||||
|
|
||||||
if restart:
|
if restart:
|
||||||
logger.info(u"Tautulli is restarting...")
|
logger.info(u"Tautulli is restarting...")
|
||||||
|
|
||||||
exe = sys.executable
|
exe = sys.executable
|
||||||
args = [exe, FULL_PATH]
|
args = [exe, FULL_PATH]
|
||||||
args += ARGS
|
args += ARGS
|
||||||
if '--nolaunch' not in args:
|
if '--nolaunch' not in args:
|
||||||
args += ['--nolaunch']
|
args += ['--nolaunch']
|
||||||
|
|
||||||
# os.execv fails with spaced names on Windows
|
# Separate out logger so we can shutdown logger after
|
||||||
# https://bugs.python.org/issue19066
|
|
||||||
if NOFORK:
|
if NOFORK:
|
||||||
logger.info('Running as service, not forking. Exiting...')
|
logger.info('Running as service, not forking. Exiting...')
|
||||||
elif os.name == 'nt':
|
elif os.name == 'nt':
|
||||||
logger.info('Restarting Tautulli with %s', args)
|
logger.info('Restarting Tautulli with %s', args)
|
||||||
subprocess.Popen(args, cwd=os.getcwd())
|
|
||||||
else:
|
else:
|
||||||
logger.info('Restarting Tautulli with %s', args)
|
logger.info('Restarting Tautulli with %s', args)
|
||||||
|
|
||||||
|
logger.shutdown()
|
||||||
|
|
||||||
|
# os.execv fails with spaced names on Windows
|
||||||
|
# https://bugs.python.org/issue19066
|
||||||
|
if NOFORK:
|
||||||
|
pass
|
||||||
|
elif os.name == 'nt':
|
||||||
|
subprocess.Popen(args, cwd=os.getcwd())
|
||||||
|
else:
|
||||||
os.execv(exe, args)
|
os.execv(exe, args)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.shutdown()
|
||||||
|
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -54,7 +54,7 @@ class ActivityHandler(object):
|
|||||||
|
|
||||||
def get_rating_key(self):
|
def get_rating_key(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
return int(self.timeline['ratingKey'])
|
return self.timeline['ratingKey']
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -65,6 +65,10 @@ class ActivityHandler(object):
|
|||||||
if session_list:
|
if session_list:
|
||||||
for session in session_list['sessions']:
|
for session in session_list['sessions']:
|
||||||
if int(session['session_key']) == self.get_session_key():
|
if int(session['session_key']) == self.get_session_key():
|
||||||
|
# Live sessions don't have rating keys in sessions
|
||||||
|
# Get it from the websocket data
|
||||||
|
if not session['rating_key']:
|
||||||
|
session['rating_key'] = self.get_rating_key()
|
||||||
return session
|
return session
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -93,14 +97,15 @@ class ActivityHandler(object):
|
|||||||
% (str(session['session_key']), str(session['user_id']), session['username'],
|
% (str(session['session_key']), str(session['user_id']), session['username'],
|
||||||
str(session['rating_key']), session['full_title']))
|
str(session['rating_key']), session['full_title']))
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': session, 'notify_action': 'on_play'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||||
|
|
||||||
# Write the new session to our temp session table
|
# Write the new session to our temp session table
|
||||||
self.update_db_session(session=session)
|
self.update_db_session(session=session)
|
||||||
|
|
||||||
def on_stop(self, force_stop=False):
|
def on_stop(self, force_stop=False):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
logger.debug(u"Tautulli ActivityHandler :: Session %s stopped." % str(self.get_session_key()))
|
logger.debug(u"Tautulli ActivityHandler :: Session %s %sstopped."
|
||||||
|
% (str(self.get_session_key()), 'force ' if force_stop else ''))
|
||||||
|
|
||||||
# Set the session last_paused timestamp
|
# Set the session last_paused timestamp
|
||||||
ap = activity_processor.ActivityProcessor()
|
ap = activity_processor.ActivityProcessor()
|
||||||
@@ -117,17 +122,23 @@ class ActivityHandler(object):
|
|||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_stop'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
|
||||||
|
|
||||||
# Write it to the history table
|
# Write it to the history table
|
||||||
monitor_proc = activity_processor.ActivityProcessor()
|
monitor_proc = activity_processor.ActivityProcessor()
|
||||||
monitor_proc.write_session_history(session=db_session)
|
row_id = monitor_proc.write_session_history(session=db_session)
|
||||||
|
|
||||||
# Remove the session from our temp session table
|
if row_id:
|
||||||
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
|
||||||
% (str(self.get_session_key()), str(self.get_rating_key())))
|
|
||||||
ap.delete_session(session_key=self.get_session_key())
|
# Remove the session from our temp session table
|
||||||
delete_metadata_cache(self.get_session_key())
|
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
|
% (str(self.get_session_key()), str(self.get_rating_key())))
|
||||||
|
ap.delete_session(row_id=row_id)
|
||||||
|
delete_metadata_cache(self.get_session_key())
|
||||||
|
else:
|
||||||
|
schedule_callback('session_key-{}'.format(self.get_session_key()), func=force_stop_stream,
|
||||||
|
args=[self.get_session_key()], seconds=30)
|
||||||
|
|
||||||
def on_pause(self, still_paused=False):
|
def on_pause(self, still_paused=False):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -148,7 +159,7 @@ class ActivityHandler(object):
|
|||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
if not still_paused:
|
if not still_paused:
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_pause'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
|
||||||
|
|
||||||
def on_resume(self):
|
def on_resume(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -167,7 +178,7 @@ class ActivityHandler(object):
|
|||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_resume'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
|
||||||
|
|
||||||
def on_buffer(self):
|
def on_buffer(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -205,7 +216,7 @@ class ActivityHandler(object):
|
|||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_buffer'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
# This function receives events from our websocket connection
|
# This function receives events from our websocket connection
|
||||||
def process(self):
|
def process(self):
|
||||||
@@ -220,7 +231,7 @@ class ActivityHandler(object):
|
|||||||
if db_session:
|
if db_session:
|
||||||
# Re-schedule the callback to reset the 5 minutes timer
|
# Re-schedule the callback to reset the 5 minutes timer
|
||||||
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
||||||
function=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
||||||
|
|
||||||
last_state = db_session['state']
|
last_state = db_session['state']
|
||||||
last_key = str(db_session['rating_key'])
|
last_key = str(db_session['rating_key'])
|
||||||
@@ -245,9 +256,6 @@ class ActivityHandler(object):
|
|||||||
elif this_state == 'stopped':
|
elif this_state == 'stopped':
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
|
|
||||||
# Remove the callback if the stream is stopped
|
|
||||||
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
|
|
||||||
|
|
||||||
elif this_state == 'buffering':
|
elif this_state == 'buffering':
|
||||||
self.on_buffer()
|
self.on_buffer()
|
||||||
|
|
||||||
@@ -271,7 +279,7 @@ class ActivityHandler(object):
|
|||||||
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_watched'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), '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.
|
||||||
@@ -280,7 +288,7 @@ class ActivityHandler(object):
|
|||||||
|
|
||||||
# Schedule a callback to force stop a stale stream 5 minutes later
|
# Schedule a callback to force stop a stale stream 5 minutes later
|
||||||
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
schedule_callback('session_key-{}'.format(self.get_session_key()),
|
||||||
function=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
func=force_stop_stream, args=[self.get_session_key()], minutes=5)
|
||||||
|
|
||||||
|
|
||||||
class TimelineHandler(object):
|
class TimelineHandler(object):
|
||||||
@@ -324,6 +332,7 @@ class TimelineHandler(object):
|
|||||||
9: 'album',
|
9: 'album',
|
||||||
10: 'track'}
|
10: 'track'}
|
||||||
|
|
||||||
|
identifier = self.timeline.get('identifier')
|
||||||
state_type = self.timeline.get('state')
|
state_type = self.timeline.get('state')
|
||||||
media_type = media_types.get(self.timeline.get('type'))
|
media_type = media_types.get(self.timeline.get('type'))
|
||||||
section_id = self.timeline.get('sectionID', 0)
|
section_id = self.timeline.get('sectionID', 0)
|
||||||
@@ -332,6 +341,10 @@ class TimelineHandler(object):
|
|||||||
media_state = self.timeline.get('mediaState')
|
media_state = self.timeline.get('mediaState')
|
||||||
queue_size = self.timeline.get('queueSize')
|
queue_size = self.timeline.get('queueSize')
|
||||||
|
|
||||||
|
# Return if it is not a library event (i.e. DVR EPG event)
|
||||||
|
if identifier != 'com.plexapp.plugins.library':
|
||||||
|
return
|
||||||
|
|
||||||
# Add a new media item to the recently added queue
|
# Add a new media item to the recently added queue
|
||||||
if media_type and section_id > 0 and \
|
if media_type and section_id > 0 and \
|
||||||
((state_type == 0 and metadata_state == 'created')): # or \
|
((state_type == 0 and metadata_state == 'created')): # or \
|
||||||
@@ -358,7 +371,7 @@ class TimelineHandler(object):
|
|||||||
% (title, str(rating_key), str(grandparent_rating_key)))
|
% (title, str(rating_key), str(grandparent_rating_key)))
|
||||||
|
|
||||||
# Schedule a callback to clear the recently added queue
|
# Schedule a callback to clear the recently added queue
|
||||||
schedule_callback('rating_key-{}'.format(grandparent_rating_key), function=clear_recently_added_queue,
|
schedule_callback('rating_key-{}'.format(grandparent_rating_key), func=clear_recently_added_queue,
|
||||||
args=[grandparent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
args=[grandparent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
elif media_type in ('season', 'album'):
|
elif media_type in ('season', 'album'):
|
||||||
@@ -374,7 +387,7 @@ class TimelineHandler(object):
|
|||||||
% (title, str(rating_key), str(parent_rating_key)))
|
% (title, str(rating_key), str(parent_rating_key)))
|
||||||
|
|
||||||
# Schedule a callback to clear the recently added queue
|
# Schedule a callback to clear the recently added queue
|
||||||
schedule_callback('rating_key-{}'.format(parent_rating_key), function=clear_recently_added_queue,
|
schedule_callback('rating_key-{}'.format(parent_rating_key), func=clear_recently_added_queue,
|
||||||
args=[parent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
args=[parent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -385,7 +398,7 @@ class TimelineHandler(object):
|
|||||||
% (title, str(rating_key)))
|
% (title, str(rating_key)))
|
||||||
|
|
||||||
# Schedule a callback to clear the recently added queue
|
# Schedule a callback to clear the recently added queue
|
||||||
schedule_callback('rating_key-{}'.format(rating_key), function=clear_recently_added_queue,
|
schedule_callback('rating_key-{}'.format(rating_key), func=clear_recently_added_queue,
|
||||||
args=[rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
args=[rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
# A movie, show, or artist is done processing
|
# A movie, show, or artist is done processing
|
||||||
@@ -415,7 +428,7 @@ def del_keys(key):
|
|||||||
del_keys(RECENTLY_ADDED_QUEUE.pop(key))
|
del_keys(RECENTLY_ADDED_QUEUE.pop(key))
|
||||||
|
|
||||||
|
|
||||||
def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs):
|
def schedule_callback(id, func=None, remove_job=False, args=None, **kwargs):
|
||||||
if ACTIVITY_SCHED.get_job(id):
|
if ACTIVITY_SCHED.get_job(id):
|
||||||
if remove_job:
|
if remove_job:
|
||||||
ACTIVITY_SCHED.remove_job(id)
|
ACTIVITY_SCHED.remove_job(id)
|
||||||
@@ -425,7 +438,7 @@ def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs):
|
|||||||
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
||||||
elif not remove_job:
|
elif not remove_job:
|
||||||
ACTIVITY_SCHED.add_job(
|
ACTIVITY_SCHED.add_job(
|
||||||
function, args=args, id=id, trigger=DateTrigger(
|
func, args=args, id=id, trigger=DateTrigger(
|
||||||
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
|
||||||
|
|
||||||
|
|
||||||
@@ -433,13 +446,14 @@ def force_stop_stream(session_key):
|
|||||||
ap = activity_processor.ActivityProcessor()
|
ap = activity_processor.ActivityProcessor()
|
||||||
session = ap.get_session_by_key(session_key=session_key)
|
session = ap.get_session_by_key(session_key=session_key)
|
||||||
|
|
||||||
success = ap.write_session_history(session=session)
|
row_id = ap.write_session_history(session=session)
|
||||||
|
|
||||||
if success:
|
if row_id:
|
||||||
# If session is written to the databaase successfully, remove the session from the session table
|
# If session is written to the database successfully, remove the session from the session table
|
||||||
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||||
% (session['session_key'], session['rating_key']))
|
% (session['session_key'], session['rating_key']))
|
||||||
ap.delete_session(session_key=session_key)
|
ap.delete_session(row_id=row_id)
|
||||||
|
delete_metadata_cache(session_key)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
session['write_attempts'] += 1
|
session['write_attempts'] += 1
|
||||||
@@ -451,7 +465,7 @@ def force_stop_stream(session_key):
|
|||||||
ap.increment_write_attempts(session_key=session_key)
|
ap.increment_write_attempts(session_key=session_key)
|
||||||
|
|
||||||
# Reschedule for 30 seconds later
|
# Reschedule for 30 seconds later
|
||||||
schedule_callback('session_key={}'.format(session_key), function=force_stop_stream,
|
schedule_callback('session_key-{}'.format(session_key), func=force_stop_stream,
|
||||||
args=[session_key], seconds=30)
|
args=[session_key], seconds=30)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -498,12 +512,12 @@ def on_created(rating_key, **kwargs):
|
|||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
notify = True
|
notify = True
|
||||||
now = int(time.time())
|
# now = int(time.time())
|
||||||
|
#
|
||||||
if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
|
# if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
|
||||||
logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
|
# logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
|
||||||
% str(rating_key))
|
# % str(rating_key))
|
||||||
notify = False
|
# notify = False
|
||||||
|
|
||||||
data_factory = datafactory.DataFactory()
|
data_factory = datafactory.DataFactory()
|
||||||
if 'child_keys' not in kwargs:
|
if 'child_keys' not in kwargs:
|
||||||
|
@@ -61,12 +61,12 @@ def check_active_sessions(ws_request=False):
|
|||||||
if session['state'] == 'paused':
|
if session['state'] == 'paused':
|
||||||
logger.debug(u"Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
logger.debug(u"Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_pause'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
|
||||||
|
|
||||||
if session['state'] == 'playing' and stream['state'] == 'paused':
|
if session['state'] == 'playing' and stream['state'] == 'paused':
|
||||||
logger.debug(u"Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
logger.debug(u"Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_resume'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
|
||||||
|
|
||||||
if stream['state'] == 'paused' and not ws_request:
|
if stream['state'] == 'paused' and not ws_request:
|
||||||
# The stream is still paused so we need to increment the paused_counter
|
# The stream is still paused so we need to increment the paused_counter
|
||||||
@@ -104,7 +104,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_buffer'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Subsequent buffer notifications after wait time
|
# Subsequent buffer notifications after wait time
|
||||||
@@ -118,7 +118,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_buffer'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
logger.debug(u"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
logger.debug(u"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||||
% (stream['session_key'],
|
% (stream['session_key'],
|
||||||
@@ -135,7 +135,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_watched'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# The user has stopped playing a stream
|
# The user has stopped playing a stream
|
||||||
@@ -155,19 +155,18 @@ def check_active_sessions(ws_request=False):
|
|||||||
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_watched'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_stop'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
|
||||||
|
|
||||||
# Write the item history on playback stop
|
# Write the item history on playback stop
|
||||||
success = monitor_process.write_session_history(session=stream)
|
row_id = monitor_process.write_session_history(session=stream)
|
||||||
|
|
||||||
if success:
|
if row_id:
|
||||||
# If session is written to the databaase successfully, remove the session from the session table
|
# If session is written to the databaase successfully, remove the session from the session table
|
||||||
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
% (stream['session_key'], stream['rating_key']))
|
% (stream['session_key'], stream['rating_key']))
|
||||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
monitor_process.delete_session(row_id=row_id)
|
||||||
[stream['session_key'], stream['rating_key']])
|
|
||||||
else:
|
else:
|
||||||
stream['write_attempts'] += 1
|
stream['write_attempts'] += 1
|
||||||
|
|
||||||
@@ -175,18 +174,14 @@ def check_active_sessions(ws_request=False):
|
|||||||
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||||
"Will try again on the next pass. Write attempt %s."
|
"Will try again on the next pass. Write attempt %s."
|
||||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||||
monitor_db.action('UPDATE sessions SET write_attempts = ? '
|
monitor_process.increment_write_attempts(session_key=stream['session_key'])
|
||||||
'WHERE session_key = ? AND rating_key = ?',
|
|
||||||
[stream['write_attempts'], stream['session_key'], stream['rating_key']])
|
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||||
"Removing session from the database. Write attempt %s."
|
"Removing session from the database. Write attempt %s."
|
||||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||||
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
% (stream['session_key'], stream['rating_key']))
|
% (stream['session_key'], stream['rating_key']))
|
||||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
monitor_process.delete_session(session_key=stream['session_key'])
|
||||||
[stream['session_key'], stream['rating_key']])
|
|
||||||
|
|
||||||
|
|
||||||
# Process the newly received session data
|
# Process the newly received session data
|
||||||
for session in media_container:
|
for session in media_container:
|
||||||
@@ -248,7 +243,7 @@ def check_recently_added():
|
|||||||
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||||
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'})
|
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
item = max(metadata, key=lambda x:x['added_at'])
|
item = max(metadata, key=lambda x:x['added_at'])
|
||||||
@@ -266,7 +261,7 @@ def check_recently_added():
|
|||||||
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
||||||
|
|
||||||
# Check if any notification agents have notifications enabled
|
# Check if any notification agents have notifications enabled
|
||||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'})
|
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||||
|
|
||||||
|
|
||||||
def check_server_response():
|
def check_server_response():
|
||||||
|
@@ -127,7 +127,7 @@ class ActivityProcessor(object):
|
|||||||
if result == 'insert':
|
if result == 'insert':
|
||||||
# Check if any notification agents have notifications enabled
|
# Check if any notification agents have notifications enabled
|
||||||
if notify:
|
if notify:
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': values, 'notify_action': 'on_play'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': values.copy(), 'notify_action': 'on_play'})
|
||||||
|
|
||||||
# If it's our first write then time stamp it.
|
# If it's our first write then time stamp it.
|
||||||
started = int(time.time())
|
started = int(time.time())
|
||||||
@@ -155,7 +155,12 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
# Reload json from raw stream info
|
# Reload json from raw stream info
|
||||||
if session.get('raw_stream_info'):
|
if session.get('raw_stream_info'):
|
||||||
session.update(json.loads(session['raw_stream_info']))
|
raw_stream_info = json.loads(session['raw_stream_info'])
|
||||||
|
# Don't overwrite id, session_key, stopped
|
||||||
|
raw_stream_info.pop('id', None)
|
||||||
|
raw_stream_info.pop('session_key', None)
|
||||||
|
raw_stream_info.pop('stopped', None)
|
||||||
|
session.update(raw_stream_info)
|
||||||
|
|
||||||
session = defaultdict(str, session)
|
session = defaultdict(str, session)
|
||||||
|
|
||||||
@@ -177,6 +182,7 @@ class ActivityProcessor(object):
|
|||||||
else:
|
else:
|
||||||
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
||||||
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
||||||
|
return session['id']
|
||||||
|
|
||||||
if str(session['paused_counter']).isdigit():
|
if str(session['paused_counter']).isdigit():
|
||||||
real_play_time = stopped - session['started'] - int(session['paused_counter'])
|
real_play_time = stopped - session['started'] - int(session['paused_counter'])
|
||||||
@@ -229,7 +235,8 @@ class ActivityProcessor(object):
|
|||||||
## TODO: Fix media info from imports. Temporary media info from import session.
|
## TODO: Fix media info from imports. Temporary media info from import session.
|
||||||
media_info = session
|
media_info = session
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history table...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write sessionKey %s to session_history table..."
|
||||||
|
# % session['session_key'])
|
||||||
keys = {'id': None}
|
keys = {'id': None}
|
||||||
values = {'started': session['started'],
|
values = {'started': session['started'],
|
||||||
'stopped': stopped,
|
'stopped': stopped,
|
||||||
@@ -254,7 +261,8 @@ class ActivityProcessor(object):
|
|||||||
'view_offset': session['view_offset']
|
'view_offset': session['view_offset']
|
||||||
}
|
}
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history transaction...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history transaction..."
|
||||||
|
# % session['session_key'])
|
||||||
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Check if we should group the session, select the last two rows from the user
|
# Check if we should group the session, select the last two rows from the user
|
||||||
@@ -284,7 +292,7 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
||||||
if prev_session == new_session == None:
|
if prev_session is None and new_session is None:
|
||||||
args = [last_id, last_id]
|
args = [last_id, last_id]
|
||||||
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
||||||
args = [prev_session['reference_id'], new_session['id']]
|
args = [prev_session['reference_id'], new_session['id']]
|
||||||
@@ -298,7 +306,8 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
# Write the session_history_media_info table
|
# Write the session_history_media_info table
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history_media_info table...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_media_info table..."
|
||||||
|
# % session['session_key'])
|
||||||
keys = {'id': last_id}
|
keys = {'id': last_id}
|
||||||
values = {'rating_key': session['rating_key'],
|
values = {'rating_key': session['rating_key'],
|
||||||
'video_decision': session['video_decision'],
|
'video_decision': session['video_decision'],
|
||||||
@@ -365,7 +374,8 @@ class ActivityProcessor(object):
|
|||||||
'optimized_version_title': session['optimized_version_title']
|
'optimized_version_title': session['optimized_version_title']
|
||||||
}
|
}
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_media_info transaction...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_media_info transaction..."
|
||||||
|
# % session['session_key'])
|
||||||
self.db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Write the session_history_metadata table
|
# Write the session_history_metadata table
|
||||||
@@ -375,7 +385,8 @@ class ActivityProcessor(object):
|
|||||||
genres = ";".join(metadata['genres'])
|
genres = ";".join(metadata['genres'])
|
||||||
labels = ";".join(metadata['labels'])
|
labels = ";".join(metadata['labels'])
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history_metadata table...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_metadata table..."
|
||||||
|
# % session['session_key'])
|
||||||
keys = {'id': last_id}
|
keys = {'id': last_id}
|
||||||
values = {'rating_key': session['rating_key'],
|
values = {'rating_key': session['rating_key'],
|
||||||
'parent_rating_key': session['parent_rating_key'],
|
'parent_rating_key': session['parent_rating_key'],
|
||||||
@@ -411,11 +422,12 @@ class ActivityProcessor(object):
|
|||||||
'labels': labels
|
'labels': labels
|
||||||
}
|
}
|
||||||
|
|
||||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_metadata transaction...")
|
# logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_metadata transaction..."
|
||||||
|
# % session['session_key'])
|
||||||
self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)
|
self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
# Return true when the session is successfully written to the database
|
# Return the session row id when the session is successfully written to the database
|
||||||
return True
|
return session['id']
|
||||||
|
|
||||||
def get_sessions(self, user_id=None, ip_address=None):
|
def get_sessions(self, user_id=None, ip_address=None):
|
||||||
query = 'SELECT * FROM sessions'
|
query = 'SELECT * FROM sessions'
|
||||||
@@ -456,9 +468,11 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_session(self, session_key=None):
|
def delete_session(self, session_key=None, row_id=None):
|
||||||
if str(session_key).isdigit():
|
if str(session_key).isdigit():
|
||||||
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
||||||
|
elif str(row_id).isdigit():
|
||||||
|
self.db.action('DELETE FROM sessions WHERE id = ?', [row_id])
|
||||||
|
|
||||||
def set_session_last_paused(self, session_key=None, timestamp=None):
|
def set_session_last_paused(self, session_key=None, timestamp=None):
|
||||||
if str(session_key).isdigit():
|
if str(session_key).isdigit():
|
||||||
|
@@ -174,11 +174,11 @@ HW_ENCODERS = [
|
|||||||
|
|
||||||
SCHEDULER_LIST = [
|
SCHEDULER_LIST = [
|
||||||
'Check GitHub for updates',
|
'Check GitHub for updates',
|
||||||
|
'Check for server response',
|
||||||
'Check for active sessions',
|
'Check for active sessions',
|
||||||
'Check for recently added items',
|
'Check for recently added items',
|
||||||
'Check for Plex updates',
|
'Check for Plex updates',
|
||||||
'Check for Plex remote access',
|
'Check for Plex remote access',
|
||||||
'Check server response',
|
|
||||||
'Refresh users list',
|
'Refresh users list',
|
||||||
'Refresh libraries list',
|
'Refresh libraries list',
|
||||||
'Refresh Plex server URLs',
|
'Refresh Plex server URLs',
|
||||||
@@ -279,16 +279,22 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{
|
{
|
||||||
'category': 'Global',
|
'category': 'Global',
|
||||||
'parameters': [
|
'parameters': [
|
||||||
{'name': 'Tautulli Version', 'type': 'str', 'value': 'plexpy_version', 'description': 'The current version of Tautulli.'},
|
{'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', 'description': 'The current version of Tautulli.'},
|
||||||
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'plexpy_branch', 'description': 'The current git branch of Tautulli.'},
|
{'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', 'description': 'The current git remote of Tautulli.'},
|
||||||
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'plexpy_commit', 'description': 'The current git commit hash of Tautulli.'},
|
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', 'description': 'The current git branch of Tautulli.'},
|
||||||
|
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', 'description': 'The current git commit hash of Tautulli.'},
|
||||||
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
|
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
|
||||||
{'name': 'Server Uptime', 'type': 'str', 'value': 'server_uptime', 'description': 'The uptime (in days, hours, mins, secs) of your Plex Server.'},
|
{'name': 'Server IP', 'type': 'str', 'value': 'server_ip', 'description': 'The connection IP address for your Plex Server.'},
|
||||||
|
{'name': 'Server Port', 'type': 'int', 'value': 'server_port', 'description': 'The connection port for your Plex Server.'},
|
||||||
|
{'name': 'Server URL', 'type': 'str', 'value': 'server_url', 'description': 'The connection URL for your Plex Server.'},
|
||||||
|
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', 'description': 'The platform of your Plex Server.'},
|
||||||
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
|
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
|
||||||
|
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
|
||||||
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
|
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
|
||||||
{'name': 'Datestamp', 'type': 'int', 'value': 'datestamp', 'description': 'The date (in date format) the notification was triggered.'},
|
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification was triggered.'},
|
||||||
{'name': 'Timestamp', 'type': 'int', 'value': 'timestamp', 'description': 'The time (in time format) the notification was triggered.'},
|
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification was triggered.'},
|
||||||
]
|
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification was triggered.'},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'category': 'Stream Details',
|
'category': 'Stream Details',
|
||||||
@@ -394,10 +400,12 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
|
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
|
||||||
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
|
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
|
||||||
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
||||||
{'name': 'Director', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
|
{'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
|
||||||
{'name': 'Writer', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
|
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
|
||||||
{'name': 'Actor', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
|
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
|
||||||
{'name': 'Genre', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
|
{'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
|
||||||
|
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
|
||||||
|
{'name': 'Collections', 'type': 'str', 'value': 'collections', 'description': 'A list of collections for the item.'},
|
||||||
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
|
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
|
||||||
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
|
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
|
||||||
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
|
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
|
||||||
@@ -477,12 +485,12 @@ NOTIFICATION_PARAMETERS = [
|
|||||||
{
|
{
|
||||||
'category': 'Tautulli Update Available',
|
'category': 'Tautulli Update Available',
|
||||||
'parameters': [
|
'parameters': [
|
||||||
{'name': 'Plexpy Update Version', 'type': 'int', 'value': 'plexpy_update_version', 'description': 'The available update version for Tautulli.'},
|
{'name': 'Tautulli Update Version', 'type': 'int', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'},
|
||||||
{'name': 'Plexpy Update Tar', 'type': 'int', 'value': 'plexpy_update_tar', 'description': 'The tar download URL for the available update.'},
|
{'name': 'Tautulli Update Tar', 'type': 'int', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'},
|
||||||
{'name': 'Plexpy Update Zip', 'type': 'int', 'value': 'plexpy_update_zip', 'description': 'The zip download URL for the available update.'},
|
{'name': 'Tautulli Update Zip', 'type': 'int', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'},
|
||||||
{'name': 'Plexpy Update Commit', 'type': 'int', 'value': 'plexpy_update_commit', 'description': 'The commit hash for the available update.'},
|
{'name': 'Tautulli Update Commit', 'type': 'int', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'},
|
||||||
{'name': 'Plexpy Update Behind', 'type': 'int', 'value': 'plexpy_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': 'Plexpy Update Changelog', 'type': 'int', 'value': 'plexpy_update_changelog', 'description': 'The changelog for the available update.'},
|
{'name': 'Tautulli Update Changelog', 'type': 'int', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@@ -61,7 +61,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PMS_PLEXPASS': (int, 'PMS', 0),
|
'PMS_PLEXPASS': (int, 'PMS', 0),
|
||||||
'PMS_PLATFORM': (str, 'PMS', ''),
|
'PMS_PLATFORM': (str, 'PMS', ''),
|
||||||
'PMS_VERSION': (str, 'PMS', ''),
|
'PMS_VERSION': (str, 'PMS', ''),
|
||||||
'PMS_UPDATE_CHANNEL': (str, 'PMS', 'public'),
|
'PMS_UPDATE_CHANNEL': (str, 'PMS', 'plex'),
|
||||||
'PMS_UPDATE_DISTRO': (str, 'PMS', ''),
|
'PMS_UPDATE_DISTRO': (str, 'PMS', ''),
|
||||||
'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''),
|
'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''),
|
||||||
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
|
||||||
@@ -876,3 +876,9 @@ class Config(object):
|
|||||||
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
|
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
|
||||||
|
|
||||||
self.CONFIG_VERSION = 9
|
self.CONFIG_VERSION = 9
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == 9:
|
||||||
|
if self.PMS_UPDATE_CHANNEL == 'plexpass':
|
||||||
|
self.PMS_UPDATE_CHANNEL = 'beta'
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = 10
|
||||||
|
@@ -698,6 +698,10 @@ class Graphs(object):
|
|||||||
series_3 = []
|
series_3 = []
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
|
if item['resolution'] not in ('4k', 'unknown'):
|
||||||
|
item['resolution'] = item['resolution'].upper()
|
||||||
|
if item['resolution'].isdigit():
|
||||||
|
item['resolution'] += 'p'
|
||||||
categories.append(item['resolution'])
|
categories.append(item['resolution'])
|
||||||
series_1.append(item['dp_count'])
|
series_1.append(item['dp_count'])
|
||||||
series_2.append(item['ds_count'])
|
series_2.append(item['ds_count'])
|
||||||
@@ -729,16 +733,18 @@ class Graphs(object):
|
|||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT ' \
|
query = 'SELECT ' \
|
||||||
|
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \
|
||||||
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
||||||
'(CASE ' \
|
'(CASE ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 360 THEN "sd" ' \
|
'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
|
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
|
||||||
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
|
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) ' \
|
||||||
|
'ELSE session_history_media_info.stream_video_resolution END) AS resolution, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||||
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
'THEN 1 ELSE 0 END) AS dp_count, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
|
||||||
@@ -758,16 +764,18 @@ class Graphs(object):
|
|||||||
result = monitor_db.select(query)
|
result = monitor_db.select(query)
|
||||||
else:
|
else:
|
||||||
query = 'SELECT ' \
|
query = 'SELECT ' \
|
||||||
|
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \
|
||||||
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
|
||||||
'(CASE ' \
|
'(CASE ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 360 THEN "sd" ' \
|
'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
|
||||||
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \
|
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
|
||||||
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \
|
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) ' \
|
||||||
|
'ELSE session_history_media_info.stream_video_resolution END) AS resolution, ' \
|
||||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
|
||||||
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
|
||||||
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
|
||||||
@@ -799,6 +807,10 @@ class Graphs(object):
|
|||||||
series_3 = []
|
series_3 = []
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
|
if item['resolution'] not in ('4k', 'unknown'):
|
||||||
|
item['resolution'] = item['resolution'].upper()
|
||||||
|
if item['resolution'].isdigit():
|
||||||
|
item['resolution'] += 'p'
|
||||||
categories.append(item['resolution'])
|
categories.append(item['resolution'])
|
||||||
series_1.append(item['dp_count'])
|
series_1.append(item['dp_count'])
|
||||||
series_2.append(item['ds_count'])
|
series_2.append(item['ds_count'])
|
||||||
|
@@ -306,6 +306,11 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
|
|||||||
# Monkey patch the run() by monkey patching the __init__ method
|
# Monkey patch the run() by monkey patching the __init__ method
|
||||||
threading.Thread.__init__ = new_init
|
threading.Thread.__init__ = new_init
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
logging.shutdown()
|
||||||
|
|
||||||
|
|
||||||
# Expose logger methods
|
# Expose logger methods
|
||||||
# Main Tautulli logger
|
# Main Tautulli logger
|
||||||
info = logger.info
|
info = logger.info
|
||||||
|
@@ -122,8 +122,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
|
|||||||
|
|
||||||
# Add on_concurrent and on_newdevice to queue if action is on_play
|
# Add on_concurrent and on_newdevice to queue if action is on_play
|
||||||
if notify_action == 'on_play':
|
if notify_action == 'on_play':
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_concurrent'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_newdevice'})
|
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_newdevice'})
|
||||||
|
|
||||||
|
|
||||||
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||||
@@ -435,20 +435,6 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
||||||
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
|
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
|
||||||
|
|
||||||
# Get the server name
|
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
|
||||||
|
|
||||||
# Get the server uptime
|
|
||||||
plex_tv = plextv.PlexTV()
|
|
||||||
server_times = plex_tv.get_server_times()
|
|
||||||
|
|
||||||
if server_times:
|
|
||||||
updated_at = server_times['updated_at']
|
|
||||||
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
|
|
||||||
else:
|
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
|
|
||||||
server_uptime = 'N/A'
|
|
||||||
|
|
||||||
# Get metadata for the item
|
# Get metadata for the item
|
||||||
if session:
|
if session:
|
||||||
rating_key = session['rating_key']
|
rating_key = session['rating_key']
|
||||||
@@ -523,10 +509,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
remaining_duration = duration - view_offset
|
remaining_duration = duration - view_offset
|
||||||
|
|
||||||
# Build Plex URL
|
# Build Plex URL
|
||||||
|
if notify_params['media_type'] == 'track':
|
||||||
|
plex_web_rating_key = notify_params['parent_rating_key']
|
||||||
|
else:
|
||||||
|
plex_web_rating_key = notify_params['rating_key']
|
||||||
|
|
||||||
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
||||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
rating_key=rating_key)
|
rating_key=plex_web_rating_key)
|
||||||
|
|
||||||
# Get media IDs from guid and build URLs
|
# Get media IDs from guid and build URLs
|
||||||
if 'imdb://' in notify_params['guid']:
|
if 'imdb://' in notify_params['guid']:
|
||||||
@@ -655,15 +646,21 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
|
|
||||||
available_params = {
|
available_params = {
|
||||||
# Global paramaters
|
# Global paramaters
|
||||||
'plexpy_version': common.VERSION_NUMBER,
|
'tautulli_version': common.VERSION_NUMBER,
|
||||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
|
||||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||||
'server_name': server_name,
|
'tautulli_commit': plexpy.CURRENT_VERSION,
|
||||||
'server_uptime': server_uptime,
|
'server_name': plexpy.CONFIG.PMS_NAME,
|
||||||
'server_version': server_times.get('version', ''),
|
'server_ip': plexpy.CONFIG.PMS_IP,
|
||||||
|
'server_port': plexpy.CONFIG.PMS_PORT,
|
||||||
|
'server_url': plexpy.CONFIG.PMS_URL,
|
||||||
|
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
|
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
||||||
|
'server_version': plexpy.CONFIG.PMS_VERSION,
|
||||||
'action': notify_action.lstrip('on_'),
|
'action': notify_action.lstrip('on_'),
|
||||||
'datestamp': arrow.now().format(date_format),
|
'datestamp': arrow.now().format(date_format),
|
||||||
'timestamp': arrow.now().format(time_format),
|
'timestamp': arrow.now().format(time_format),
|
||||||
|
'unixtime': int(time.time()),
|
||||||
# Stream parameters
|
# Stream parameters
|
||||||
'streams': stream_count,
|
'streams': stream_count,
|
||||||
'user_streams': user_stream_count,
|
'user_streams': user_stream_count,
|
||||||
@@ -772,6 +769,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
'writers': ', '.join(notify_params['writers']),
|
'writers': ', '.join(notify_params['writers']),
|
||||||
'actors': ', '.join(notify_params['actors']),
|
'actors': ', '.join(notify_params['actors']),
|
||||||
'genres': ', '.join(notify_params['genres']),
|
'genres': ', '.join(notify_params['genres']),
|
||||||
|
'labels': ', '.join(notify_params['labels']),
|
||||||
|
'collections': ', '.join(notify_params['collections']),
|
||||||
'summary': notify_params['summary'],
|
'summary': notify_params['summary'],
|
||||||
'tagline': notify_params['tagline'],
|
'tagline': notify_params['tagline'],
|
||||||
'rating': notify_params['rating'],
|
'rating': notify_params['rating'],
|
||||||
@@ -840,40 +839,34 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||||||
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
||||||
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
||||||
|
|
||||||
# Get the server name
|
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
||||||
server_name = plexpy.CONFIG.PMS_NAME
|
|
||||||
|
|
||||||
# Get the server uptime
|
|
||||||
plex_tv = plextv.PlexTV()
|
|
||||||
server_times = plex_tv.get_server_times()
|
|
||||||
|
|
||||||
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
|
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
|
||||||
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
|
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
|
||||||
|
|
||||||
if server_times:
|
|
||||||
updated_at = server_times['updated_at']
|
|
||||||
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
|
|
||||||
else:
|
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
|
|
||||||
server_uptime = 'N/A'
|
|
||||||
|
|
||||||
available_params = {
|
available_params = {
|
||||||
# Global paramaters
|
# Global paramaters
|
||||||
'plexpy_version': common.VERSION_NUMBER,
|
'tautulli_version': common.VERSION_NUMBER,
|
||||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
|
||||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||||
'server_name': server_name,
|
'tautulli_commit': plexpy.CURRENT_VERSION,
|
||||||
'server_uptime': server_uptime,
|
'server_name': plexpy.CONFIG.PMS_NAME,
|
||||||
'server_version': server_times.get('version', ''),
|
'server_ip': plexpy.CONFIG.PMS_IP,
|
||||||
|
'server_port': plexpy.CONFIG.PMS_PORT,
|
||||||
|
'server_url': plexpy.CONFIG.PMS_URL,
|
||||||
|
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
||||||
|
'server_version': plexpy.CONFIG.PMS_VERSION,
|
||||||
|
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
'action': notify_action.lstrip('on_'),
|
'action': notify_action.lstrip('on_'),
|
||||||
'datestamp': arrow.now().format(date_format),
|
'datestamp': arrow.now().format(date_format),
|
||||||
'timestamp': arrow.now().format(time_format),
|
'timestamp': arrow.now().format(time_format),
|
||||||
|
'unixtime': int(time.time()),
|
||||||
# Plex Media Server update parameters
|
# Plex Media Server update parameters
|
||||||
'update_version': pms_download_info['version'],
|
'update_version': pms_download_info['version'],
|
||||||
'update_url': pms_download_info['download_url'],
|
'update_url': pms_download_info['download_url'],
|
||||||
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
|
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
|
||||||
if pms_download_info['release_date'] else '',
|
if pms_download_info['release_date'] else '',
|
||||||
'update_channel': 'Beta' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public',
|
'update_channel': 'Beta' if update_channel == 'beta' else 'Public',
|
||||||
'update_platform': pms_download_info['platform'],
|
'update_platform': pms_download_info['platform'],
|
||||||
'update_distro': pms_download_info['distro'],
|
'update_distro': pms_download_info['distro'],
|
||||||
'update_distro_build': pms_download_info['build'],
|
'update_distro_build': pms_download_info['build'],
|
||||||
@@ -882,12 +875,12 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||||||
'update_changelog_added': pms_download_info['changelog_added'],
|
'update_changelog_added': pms_download_info['changelog_added'],
|
||||||
'update_changelog_fixed': pms_download_info['changelog_fixed'],
|
'update_changelog_fixed': pms_download_info['changelog_fixed'],
|
||||||
# Tautulli update parameters
|
# Tautulli update parameters
|
||||||
'plexpy_update_version': plexpy_download_info['tag_name'],
|
'tautulli_update_version': plexpy_download_info['tag_name'],
|
||||||
'plexpy_update_tar': plexpy_download_info['tarball_url'],
|
'tautulli_update_tar': plexpy_download_info['tarball_url'],
|
||||||
'plexpy_update_zip': plexpy_download_info['zipball_url'],
|
'tautulli_update_zip': plexpy_download_info['zipball_url'],
|
||||||
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''),
|
'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),
|
||||||
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''),
|
'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''),
|
||||||
'plexpy_update_changelog': plexpy_download_info['body']
|
'tautulli_update_changelog': plexpy_download_info['body']
|
||||||
}
|
}
|
||||||
|
|
||||||
return available_params
|
return available_params
|
||||||
|
@@ -64,6 +64,9 @@ import users
|
|||||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||||
|
|
||||||
|
|
||||||
|
BROWSER_NOTIFIERS = {}
|
||||||
|
|
||||||
|
|
||||||
AGENT_IDS = {'growl': 0,
|
AGENT_IDS = {'growl': 0,
|
||||||
'prowl': 1,
|
'prowl': 1,
|
||||||
'xbmc': 2,
|
'xbmc': 2,
|
||||||
@@ -551,6 +554,10 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
|||||||
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
||||||
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
|
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
|
||||||
blacklist_logger()
|
blacklist_logger()
|
||||||
|
|
||||||
|
if agent['name'] == 'browser':
|
||||||
|
check_browser_enabled()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli Notifiers :: Unable to update notification agent: %s." % e)
|
logger.warn(u"Tautulli Notifiers :: Unable to update notification agent: %s." % e)
|
||||||
@@ -994,40 +1001,15 @@ class BROWSER(Notifier):
|
|||||||
Browser notifications
|
Browser notifications
|
||||||
"""
|
"""
|
||||||
NAME = 'Browser'
|
NAME = 'Browser'
|
||||||
_DEFAULT_CONFIG = {'enabled': 0,
|
_DEFAULT_CONFIG = {'auto_hide_delay': 5
|
||||||
'auto_hide_delay': 5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||||
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_notifications(self):
|
|
||||||
if not self.config['enabled']:
|
|
||||||
return
|
|
||||||
|
|
||||||
db = database.MonitorDatabase()
|
|
||||||
result = db.select('SELECT subject_text, body_text FROM notify_log '
|
|
||||||
'WHERE agent_id = 17 AND timestamp >= ? ',
|
|
||||||
args=[time.time() - 3])
|
|
||||||
|
|
||||||
notifications = []
|
|
||||||
for item in result:
|
|
||||||
notification = {'subject_text': item['subject_text'],
|
|
||||||
'body_text': item['body_text'],
|
|
||||||
'delay': self.config['auto_hide_delay']}
|
|
||||||
notifications.append(notification)
|
|
||||||
|
|
||||||
return {'notifications': notifications}
|
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'Enable Browser Notifications',
|
config_option = [{'label': 'Allow Notifications',
|
||||||
'value': self.config['enabled'],
|
|
||||||
'name': 'browser_enabled',
|
|
||||||
'description': 'Enable to display desktop notifications from your browser.',
|
|
||||||
'input_type': 'checkbox'
|
|
||||||
},
|
|
||||||
{'label': 'Allow Notifications',
|
|
||||||
'value': 'Allow Notifications',
|
'value': 'Allow Notifications',
|
||||||
'name': 'browser_allow_browser',
|
'name': 'browser_allow_browser',
|
||||||
'description': 'Click to allow browser notifications. You must click this button for each browser.',
|
'description': 'Click to allow browser notifications. You must click this button for each browser.',
|
||||||
@@ -1258,13 +1240,13 @@ class EMAIL(Notifier):
|
|||||||
|
|
||||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||||
if self.config['html_support']:
|
if self.config['html_support']:
|
||||||
body = body.replace('\n', '<br />')
|
|
||||||
msg = MIMEMultipart('alternative')
|
msg = MIMEMultipart('alternative')
|
||||||
msg.attach(MIMEText(bleach.clean(body, strip=True), 'plain', 'utf-8'))
|
msg.attach(MIMEText(bleach.clean(body, strip=True), 'plain', 'utf-8'))
|
||||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
||||||
else:
|
else:
|
||||||
msg = MIMEText(body, 'plain', 'utf-8')
|
msg = MIMEText(body, 'plain', 'utf-8')
|
||||||
|
|
||||||
|
msg['Date'] = email.utils.formatdate(localtime=True)
|
||||||
msg['Subject'] = subject
|
msg['Subject'] = subject
|
||||||
msg['From'] = email.utils.formataddr((self.config['from_name'], self.config['from']))
|
msg['From'] = email.utils.formataddr((self.config['from_name'], self.config['from']))
|
||||||
msg['To'] = ','.join(self.config['to'])
|
msg['To'] = ','.join(self.config['to'])
|
||||||
@@ -1293,8 +1275,25 @@ class EMAIL(Notifier):
|
|||||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_user_emails(self):
|
||||||
|
emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']}
|
||||||
|
|
||||||
|
user_emails_to = {v: '' for v in self.config['to']}
|
||||||
|
user_emails_cc = {v: '' for v in self.config['cc']}
|
||||||
|
user_emails_bcc = {v: '' for v in self.config['bcc']}
|
||||||
|
|
||||||
|
user_emails_to.update(emails)
|
||||||
|
user_emails_cc.update(emails)
|
||||||
|
user_emails_bcc.update(emails)
|
||||||
|
|
||||||
|
user_emails_to = [{'value': k, 'text': v} for k, v in user_emails_to.iteritems()]
|
||||||
|
user_emails_cc = [{'value': k, 'text': v} for k, v in user_emails_cc.iteritems()]
|
||||||
|
user_emails_bcc = [{'value': k, 'text': v} for k, v in user_emails_bcc.iteritems()]
|
||||||
|
|
||||||
|
return user_emails_to, user_emails_cc, user_emails_bcc
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
user_emails = {} # User selection set with selectize options
|
user_emails_to, user_emails_cc, user_emails_bcc = self.get_user_emails()
|
||||||
|
|
||||||
config_option = [{'label': 'From Name',
|
config_option = [{'label': 'From Name',
|
||||||
'value': self.config['from_name'],
|
'value': self.config['from_name'],
|
||||||
@@ -1312,22 +1311,22 @@ class EMAIL(Notifier):
|
|||||||
'value': self.config['to'],
|
'value': self.config['to'],
|
||||||
'name': 'email_to',
|
'name': 'email_to',
|
||||||
'description': 'The email address(es) of the recipients.',
|
'description': 'The email address(es) of the recipients.',
|
||||||
'input_type': 'select',
|
'input_type': 'selectize',
|
||||||
'select_options': user_emails
|
'select_options': user_emails_to
|
||||||
},
|
},
|
||||||
{'label': 'CC',
|
{'label': 'CC',
|
||||||
'value': self.config['cc'],
|
'value': self.config['cc'],
|
||||||
'name': 'email_cc',
|
'name': 'email_cc',
|
||||||
'description': 'The email address(es) to CC.',
|
'description': 'The email address(es) to CC.',
|
||||||
'input_type': 'select',
|
'input_type': 'selectize',
|
||||||
'select_options': user_emails
|
'select_options': user_emails_cc
|
||||||
},
|
},
|
||||||
{'label': 'BCC',
|
{'label': 'BCC',
|
||||||
'value': self.config['bcc'],
|
'value': self.config['bcc'],
|
||||||
'name': 'email_bcc',
|
'name': 'email_bcc',
|
||||||
'description': 'The email address(es) to BCC.',
|
'description': 'The email address(es) to BCC.',
|
||||||
'input_type': 'select',
|
'input_type': 'selectize',
|
||||||
'select_options': user_emails
|
'select_options': user_emails_bcc
|
||||||
},
|
},
|
||||||
{'label': 'SMTP Server',
|
{'label': 'SMTP Server',
|
||||||
'value': self.config['smtp_server'],
|
'value': self.config['smtp_server'],
|
||||||
@@ -1362,8 +1361,7 @@ class EMAIL(Notifier):
|
|||||||
{'label': 'Enable HTML Support',
|
{'label': 'Enable HTML Support',
|
||||||
'value': self.config['html_support'],
|
'value': self.config['html_support'],
|
||||||
'name': 'email_html_support',
|
'name': 'email_html_support',
|
||||||
'description': 'Style your messages using HTML tags. '
|
'description': 'Style your messages using HTML tags.',
|
||||||
'Line breaks (<br>) will be inserted automatically.',
|
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -3527,3 +3525,27 @@ def upgrade_config_to_db():
|
|||||||
notifier_id = add_notifier_config(agent_id=agent_id)
|
notifier_id = add_notifier_config(agent_id=agent_id)
|
||||||
set_notifier_config(notifier_id=notifier_id, agent_id=agent_id, **notifier_config)
|
set_notifier_config(notifier_id=notifier_id, agent_id=agent_id, **notifier_config)
|
||||||
|
|
||||||
|
|
||||||
|
def check_browser_enabled():
|
||||||
|
global BROWSER_NOTIFIERS
|
||||||
|
BROWSER_NOTIFIERS = {}
|
||||||
|
for n in get_notifiers():
|
||||||
|
if n['agent_id'] == 17 and n['active']:
|
||||||
|
notifier_config = get_notifier_config(n['id'])
|
||||||
|
BROWSER_NOTIFIERS[n['id']] = notifier_config['config']['auto_hide_delay']
|
||||||
|
|
||||||
|
|
||||||
|
def get_browser_notifications():
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
result = db.select('SELECT notifier_id, subject_text, body_text FROM notify_log '
|
||||||
|
'WHERE agent_id = 17 AND timestamp >= ? ',
|
||||||
|
args=[time.time() - 5])
|
||||||
|
|
||||||
|
notifications = []
|
||||||
|
for item in result:
|
||||||
|
notification = {'subject_text': item['subject_text'],
|
||||||
|
'body_text': item['body_text'],
|
||||||
|
'delay': BROWSER_NOTIFIERS.get(item['notifier_id'], 5)}
|
||||||
|
notifications.append(notification)
|
||||||
|
|
||||||
|
return {'notifications': notifications}
|
||||||
|
@@ -379,6 +379,16 @@ class PlexTV(object):
|
|||||||
if machine_id is None:
|
if machine_id is None:
|
||||||
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||||
|
|
||||||
|
if isinstance(rating_key_filter, list):
|
||||||
|
rating_key_filter = [str(k) for k in rating_key_filter]
|
||||||
|
elif rating_key_filter is not None:
|
||||||
|
rating_key_filter = [str(rating_key_filter)]
|
||||||
|
|
||||||
|
if isinstance(user_id_filter, list):
|
||||||
|
user_id_filter = [str(k) for k in user_id_filter]
|
||||||
|
elif user_id_filter is not None:
|
||||||
|
user_id_filter = [str(user_id_filter)]
|
||||||
|
|
||||||
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
|
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
|
||||||
user_data = users.Users()
|
user_data = users.Users()
|
||||||
|
|
||||||
@@ -418,7 +428,7 @@ class PlexTV(object):
|
|||||||
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
|
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
|
||||||
|
|
||||||
# Filter by user_id
|
# Filter by user_id
|
||||||
if user_id_filter and str(user_id_filter) != device_user_id:
|
if user_id_filter and device_user_id not in user_id_filter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for synced in a.getElementsByTagName('SyncItems'):
|
for synced in a.getElementsByTagName('SyncItems'):
|
||||||
@@ -432,7 +442,7 @@ class PlexTV(object):
|
|||||||
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
|
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
|
||||||
|
|
||||||
# Filter by rating_key
|
# Filter by rating_key
|
||||||
if rating_key_filter and str(rating_key_filter) != rating_key:
|
if rating_key_filter and rating_key not in rating_key_filter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sync_id = helpers.get_xml_attr(item, 'id')
|
sync_id = helpers.get_xml_attr(item, 'id')
|
||||||
@@ -461,12 +471,13 @@ class PlexTV(object):
|
|||||||
status_item_downloaded_count, status_item_count)
|
status_item_downloaded_count, status_item_count)
|
||||||
|
|
||||||
for settings in item.getElementsByTagName('MediaSettings'):
|
for settings in item.getElementsByTagName('MediaSettings'):
|
||||||
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
|
settings_video_bitrate = helpers.get_xml_attr(settings, 'maxVideoBitrate')
|
||||||
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
|
|
||||||
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
|
||||||
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
|
||||||
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
|
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
|
||||||
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
|
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
|
||||||
|
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
|
||||||
|
settings_audio_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
|
||||||
|
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
||||||
|
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
||||||
|
|
||||||
sync_details = {"device_name": helpers.sanitize(device_name),
|
sync_details = {"device_name": helpers.sanitize(device_name),
|
||||||
"platform": helpers.sanitize(device_platform),
|
"platform": helpers.sanitize(device_platform),
|
||||||
@@ -483,7 +494,8 @@ class PlexTV(object):
|
|||||||
"item_complete_count": status_item_complete_count,
|
"item_complete_count": status_item_complete_count,
|
||||||
"item_downloaded_count": status_item_downloaded_count,
|
"item_downloaded_count": status_item_downloaded_count,
|
||||||
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
||||||
"music_bitrate": settings_music_bitrate,
|
"video_bitrate": settings_video_bitrate,
|
||||||
|
"audio_bitrate": settings_audio_bitrate,
|
||||||
"photo_quality": settings_photo_quality,
|
"photo_quality": settings_photo_quality,
|
||||||
"video_quality": settings_video_quality,
|
"video_quality": settings_video_quality,
|
||||||
"total_size": status_total_size,
|
"total_size": status_total_size,
|
||||||
@@ -641,10 +653,14 @@ class PlexTV(object):
|
|||||||
|
|
||||||
def get_plex_downloads(self):
|
def get_plex_downloads(self):
|
||||||
logger.debug(u"Tautulli PlexTV :: Retrieving current server version.")
|
logger.debug(u"Tautulli PlexTV :: Retrieving current server version.")
|
||||||
pmsconnect.PmsConnect().set_server_version()
|
|
||||||
|
|
||||||
logger.debug(u"Tautulli PlexTV :: Plex update channel is %s." % plexpy.CONFIG.PMS_UPDATE_CHANNEL)
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
plex_downloads = self.get_plextv_downloads(plexpass=(plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass'))
|
pms_connect.set_server_version()
|
||||||
|
|
||||||
|
update_channel = pms_connect.get_server_update_channel()
|
||||||
|
|
||||||
|
logger.debug(u"Tautulli PlexTV :: Plex update channel is %s." % update_channel)
|
||||||
|
plex_downloads = self.get_plextv_downloads(plexpass=(update_channel == 'beta'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
available_downloads = json.loads(plex_downloads)
|
available_downloads = json.loads(plex_downloads)
|
||||||
|
@@ -537,7 +537,7 @@ class PmsConnect(object):
|
|||||||
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)
|
||||||
except IOError as e:
|
except (IOError, ValueError) as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
@@ -559,27 +559,32 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
if a.getAttribute('size'):
|
if a.getAttribute('size'):
|
||||||
if a.getAttribute('size') != '1':
|
if a.getAttribute('size') == '0':
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if a.getElementsByTagName('Directory'):
|
if a.getElementsByTagName('Directory'):
|
||||||
metadata_main = a.getElementsByTagName('Directory')[0]
|
metadata_main_list = a.getElementsByTagName('Directory')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
if metadata_type == 'photo':
|
|
||||||
metadata_type = 'photo_album'
|
|
||||||
elif a.getElementsByTagName('Video'):
|
elif a.getElementsByTagName('Video'):
|
||||||
metadata_main = a.getElementsByTagName('Video')[0]
|
metadata_main_list = a.getElementsByTagName('Video')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
elif a.getElementsByTagName('Track'):
|
elif a.getElementsByTagName('Track'):
|
||||||
metadata_main = a.getElementsByTagName('Track')[0]
|
metadata_main_list = a.getElementsByTagName('Track')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
elif a.getElementsByTagName('Photo'):
|
elif a.getElementsByTagName('Photo'):
|
||||||
metadata_main = a.getElementsByTagName('Photo')[0]
|
metadata_main_list = a.getElementsByTagName('Photo')
|
||||||
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
|
||||||
else:
|
else:
|
||||||
logger.debug(u"Tautulli Pmsconnect :: Metadata failed")
|
logger.debug(u"Tautulli Pmsconnect :: Metadata failed")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
if sync_id and len(metadata_main_list) > 1:
|
||||||
|
for metadata_main in metadata_main_list:
|
||||||
|
if helpers.get_xml_attr(metadata_main, 'ratingKey') == rating_key:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
metadata_main = metadata_main_list[0]
|
||||||
|
|
||||||
|
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
|
||||||
|
if metadata_type == 'photo':
|
||||||
|
metadata_type = 'photo_album'
|
||||||
|
|
||||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||||
library_name = helpers.get_xml_attr(a, 'librarySectionTitle')
|
library_name = helpers.get_xml_attr(a, 'librarySectionTitle')
|
||||||
|
|
||||||
@@ -588,6 +593,7 @@ class PmsConnect(object):
|
|||||||
actors = []
|
actors = []
|
||||||
genres = []
|
genres = []
|
||||||
labels = []
|
labels = []
|
||||||
|
collections = []
|
||||||
|
|
||||||
if metadata_main.getElementsByTagName('Director'):
|
if metadata_main.getElementsByTagName('Director'):
|
||||||
for director in metadata_main.getElementsByTagName('Director'):
|
for director in metadata_main.getElementsByTagName('Director'):
|
||||||
@@ -609,6 +615,10 @@ class PmsConnect(object):
|
|||||||
for label in metadata_main.getElementsByTagName('Label'):
|
for label in metadata_main.getElementsByTagName('Label'):
|
||||||
labels.append(helpers.get_xml_attr(label, 'tag'))
|
labels.append(helpers.get_xml_attr(label, 'tag'))
|
||||||
|
|
||||||
|
if metadata_main.getElementsByTagName('Collection'):
|
||||||
|
for collection in metadata_main.getElementsByTagName('Collection'):
|
||||||
|
collections.append(helpers.get_xml_attr(collection, 'tag'))
|
||||||
|
|
||||||
if metadata_type == 'movie':
|
if metadata_type == 'movie':
|
||||||
metadata = {'media_type': metadata_type,
|
metadata = {'media_type': metadata_type,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
@@ -646,6 +656,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +697,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,6 +740,7 @@ class PmsConnect(object):
|
|||||||
'actors': show_details['actors'],
|
'actors': show_details['actors'],
|
||||||
'genres': show_details['genres'],
|
'genres': show_details['genres'],
|
||||||
'labels': show_details['labels'],
|
'labels': show_details['labels'],
|
||||||
|
'collections': show_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -771,6 +784,7 @@ class PmsConnect(object):
|
|||||||
'actors': show_details['actors'],
|
'actors': show_details['actors'],
|
||||||
'genres': show_details['genres'],
|
'genres': show_details['genres'],
|
||||||
'labels': show_details['labels'],
|
'labels': show_details['labels'],
|
||||||
|
'collections': show_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -812,6 +826,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,6 +869,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -897,6 +913,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': album_details['genres'],
|
'genres': album_details['genres'],
|
||||||
'labels': album_details['labels'],
|
'labels': album_details['labels'],
|
||||||
|
'collections': album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -938,6 +955,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,6 +998,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': photo_album_details['genres'],
|
'genres': photo_album_details['genres'],
|
||||||
'labels': photo_album_details['labels'],
|
'labels': photo_album_details['labels'],
|
||||||
|
'collections': photo_album_details['collections'],
|
||||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
helpers.get_xml_attr(metadata_main, 'title'))
|
helpers.get_xml_attr(metadata_main, 'title'))
|
||||||
}
|
}
|
||||||
@@ -1025,6 +1044,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,6 +1085,7 @@ class PmsConnect(object):
|
|||||||
'actors': actors,
|
'actors': actors,
|
||||||
'genres': genres,
|
'genres': genres,
|
||||||
'labels': labels,
|
'labels': labels,
|
||||||
|
'collections': collections,
|
||||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,7 +1183,7 @@ class PmsConnect(object):
|
|||||||
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)
|
||||||
except IOError as e:
|
except (IOError, ValueError) as e:
|
||||||
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
|
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
|
||||||
% (cache_key, e))
|
% (cache_key, e))
|
||||||
|
|
||||||
@@ -1370,7 +1391,7 @@ class PmsConnect(object):
|
|||||||
else:
|
else:
|
||||||
session_details = {'session_id': '',
|
session_details = {'session_id': '',
|
||||||
'bandwidth': '',
|
'bandwidth': '',
|
||||||
'location': 'Unknown'
|
'location': 'wan' if player_details['local'] == '0' else 'lan'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the transcode details
|
# Get the transcode details
|
||||||
@@ -1443,16 +1464,24 @@ class PmsConnect(object):
|
|||||||
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
|
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
|
||||||
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
|
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
|
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
|
||||||
|
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
|
||||||
|
|
||||||
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
|
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
|
||||||
rating_key_filter=rating_key)
|
rating_key_filter=[rating_key, parent_rating_key, grandparent_rating_key])
|
||||||
if synced_items:
|
if synced_items:
|
||||||
sync_id = synced_items[0]['sync_id']
|
synced_item_details = synced_items[0]
|
||||||
|
sync_id = synced_item_details['sync_id']
|
||||||
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
|
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
|
||||||
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
|
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
|
||||||
if synced_xml_head[0].getElementsByTagName('Track'):
|
if synced_xml_head[0].getElementsByTagName('Track'):
|
||||||
synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0]
|
synced_xml_items = synced_xml_head[0].getElementsByTagName('Track')
|
||||||
elif synced_xml_head[0].getElementsByTagName('Video'):
|
elif synced_xml_head[0].getElementsByTagName('Video'):
|
||||||
synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0]
|
synced_xml_items = synced_xml_head[0].getElementsByTagName('Video')
|
||||||
|
|
||||||
|
for synced_session_data in synced_xml_items:
|
||||||
|
if helpers.get_xml_attr(synced_session_data, 'ratingKey') == rating_key:
|
||||||
|
break
|
||||||
|
|
||||||
# Figure out which version is being played
|
# Figure out which version is being played
|
||||||
if sync_id:
|
if sync_id:
|
||||||
@@ -1586,6 +1615,7 @@ class PmsConnect(object):
|
|||||||
channel_stream = 1
|
channel_stream = 1
|
||||||
|
|
||||||
clip_media = session.getElementsByTagName('Media')[0]
|
clip_media = session.getElementsByTagName('Media')[0]
|
||||||
|
clip_part = clip_media.getElementsByTagName('Part')[0]
|
||||||
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
|
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
|
||||||
metadata_details = {'media_type': media_type,
|
metadata_details = {'media_type': media_type,
|
||||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||||
@@ -1624,7 +1654,8 @@ class PmsConnect(object):
|
|||||||
'genres': [],
|
'genres': [],
|
||||||
'labels': [],
|
'labels': [],
|
||||||
'full_title': helpers.get_xml_attr(session, 'title'),
|
'full_title': helpers.get_xml_attr(session, 'title'),
|
||||||
'container': helpers.get_xml_attr(clip_media, 'container'),
|
'container': helpers.get_xml_attr(clip_media, 'container') \
|
||||||
|
or helpers.get_xml_attr(clip_part, 'container'),
|
||||||
'height': helpers.get_xml_attr(clip_media, 'height'),
|
'height': helpers.get_xml_attr(clip_media, 'height'),
|
||||||
'width': helpers.get_xml_attr(clip_media, 'width'),
|
'width': helpers.get_xml_attr(clip_media, 'width'),
|
||||||
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
|
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
|
||||||
@@ -1633,7 +1664,8 @@ class PmsConnect(object):
|
|||||||
'audio_channels': audio_channels,
|
'audio_channels': audio_channels,
|
||||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||||
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
||||||
'channel_title': helpers.get_xml_attr(session, 'sourceTitle')
|
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
|
||||||
|
'live': int(helpers.get_xml_attr(session, 'live') == '1')
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
channel_stream = 0
|
channel_stream = 0
|
||||||
@@ -1642,7 +1674,7 @@ class PmsConnect(object):
|
|||||||
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
|
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
|
||||||
|
|
||||||
if sync_id:
|
if sync_id:
|
||||||
metadata_details = self.get_metadata_details(sync_id=sync_id, cache_key=session_key)
|
metadata_details = self.get_metadata_details(rating_key=rating_key, sync_id=sync_id, cache_key=session_key)
|
||||||
else:
|
else:
|
||||||
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
|
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
|
||||||
|
|
||||||
@@ -1699,51 +1731,72 @@ class PmsConnect(object):
|
|||||||
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
|
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
|
||||||
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
|
||||||
|
|
||||||
|
# Overrides for live sessions
|
||||||
|
if metadata_details.get('live') and transcode_decision == 'transcode':
|
||||||
|
stream_details['stream_container_decision'] = 'transcode'
|
||||||
|
stream_details['stream_container'] = transcode_details['transcode_container']
|
||||||
|
|
||||||
|
video_details['stream_video_decision'] = transcode_details['video_decision']
|
||||||
|
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
|
||||||
|
stream_details['stream_video_resolution'] = metadata_details['video_resolution']
|
||||||
|
|
||||||
|
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
|
||||||
|
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
|
||||||
|
stream_details['stream_audio_channels'] = transcode_details['transcode_audio_channels']
|
||||||
|
stream_details['stream_audio_channel_layout'] = common.AUDIO_CHANNELS.get(
|
||||||
|
transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels'])
|
||||||
|
|
||||||
# Get the quality profile
|
# Get the quality profile
|
||||||
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
||||||
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
if sync_id:
|
||||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
quailtiy_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
|
||||||
quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate]
|
|
||||||
except ValueError:
|
|
||||||
quality_profile = 'Original'
|
quality_profile = 'Original'
|
||||||
|
|
||||||
if sync_id:
|
synced_item_bitrate = helpers.cast_to_int(synced_item_details['video_bitrate'])
|
||||||
try:
|
try:
|
||||||
synced_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if source_bitrate <= b)
|
synced_bitrate = max(b for b in common.VIDEO_QUALITY_PROFILES if b <= synced_item_bitrate)
|
||||||
synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate]
|
synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
synced_version_profile = 'Original'
|
synced_version_profile = 'Original'
|
||||||
else:
|
else:
|
||||||
synced_version_profile = ''
|
synced_version_profile = ''
|
||||||
|
|
||||||
|
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
||||||
|
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||||
|
try:
|
||||||
|
quailtiy_bitrate = min(
|
||||||
|
b for b in common.VIDEO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
||||||
|
quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate]
|
||||||
|
except ValueError:
|
||||||
|
quality_profile = 'Original'
|
||||||
|
|
||||||
if stream_details['optimized_version']:
|
if stream_details['optimized_version']:
|
||||||
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
||||||
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'], source_media_details['video_resolution']))
|
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'],
|
||||||
|
source_media_details['video_resolution']))
|
||||||
else:
|
else:
|
||||||
optimized_version_profile = ''
|
optimized_version_profile = ''
|
||||||
|
|
||||||
elif media_type == 'track' and 'stream_bitrate' in stream_details:
|
elif media_type == 'track' and 'stream_bitrate' in stream_details:
|
||||||
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
if sync_id:
|
||||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
|
||||||
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
|
|
||||||
except ValueError:
|
|
||||||
quality_profile = 'Original'
|
quality_profile = 'Original'
|
||||||
|
|
||||||
if sync_id:
|
synced_item_bitrate = helpers.cast_to_int(synced_item_details['audio_bitrate'])
|
||||||
try:
|
try:
|
||||||
synced_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if source_bitrate <= b)
|
synced_bitrate = max(b for b in common.AUDIO_QUALITY_PROFILES if b <= synced_item_bitrate)
|
||||||
synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate]
|
synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
synced_version_profile = 'Original'
|
synced_version_profile = 'Original'
|
||||||
else:
|
else:
|
||||||
synced_version_profile = ''
|
synced_version_profile = ''
|
||||||
|
|
||||||
|
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
|
||||||
|
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||||
|
try:
|
||||||
|
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
|
||||||
|
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
|
||||||
|
except ValueError:
|
||||||
|
quality_profile = 'Original'
|
||||||
|
|
||||||
optimized_version_profile = ''
|
optimized_version_profile = ''
|
||||||
|
|
||||||
elif media_type == 'photo':
|
elif media_type == 'photo':
|
||||||
@@ -2534,3 +2587,14 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
plexpy.CONFIG.__setattr__('PMS_VERSION', version)
|
plexpy.CONFIG.__setattr__('PMS_VERSION', version)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
|
def get_server_update_channel(self):
|
||||||
|
if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plex':
|
||||||
|
update_channel_value = self.get_server_pref('ButlerUpdateChannel')
|
||||||
|
|
||||||
|
if update_channel_value == '8':
|
||||||
|
return 'beta'
|
||||||
|
else:
|
||||||
|
return 'public'
|
||||||
|
|
||||||
|
return plexpy.CONFIG.PMS_UPDATE_CHANNEL
|
||||||
|
@@ -597,7 +597,7 @@ class Users(object):
|
|||||||
for item in result:
|
for item in result:
|
||||||
user = {'user_id': item['user_id'],
|
user = {'user_id': item['user_id'],
|
||||||
'username': item['username'],
|
'username': item['username'],
|
||||||
'friendly_name': item['friendly_name'],
|
'friendly_name': item['friendly_name'] or item['username'],
|
||||||
'email': item['email']
|
'email': item['email']
|
||||||
}
|
}
|
||||||
users.append(user)
|
users.append(user)
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "beta"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.0.13-beta"
|
PLEXPY_RELEASE_VERSION = "v2.0.16-beta"
|
||||||
|
@@ -298,14 +298,14 @@ def checkout_git_branch():
|
|||||||
logger.info('Output: ' + str(output))
|
logger.info('Output: ' + str(output))
|
||||||
|
|
||||||
|
|
||||||
def read_changelog(latest_only=False):
|
def read_changelog(latest_only=False, since_prev_release=False):
|
||||||
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
||||||
|
|
||||||
if not os.path.isfile(changelog_file):
|
if not os.path.isfile(changelog_file):
|
||||||
return '<h4>Missing changelog file</h4>'
|
return '<h4>Missing changelog file</h4>'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output = ''
|
output = ['']
|
||||||
prev_level = 0
|
prev_level = 0
|
||||||
|
|
||||||
latest_version_found = False
|
latest_version_found = False
|
||||||
@@ -329,27 +329,34 @@ def read_changelog(latest_only=False):
|
|||||||
break
|
break
|
||||||
elif latest_only:
|
elif latest_only:
|
||||||
latest_version_found = True
|
latest_version_found = True
|
||||||
|
# Add a space to the end of the release to match tags
|
||||||
|
elif since_prev_release and str(plexpy.PREV_RELEASE) + ' ' in header_text:
|
||||||
|
break
|
||||||
|
|
||||||
output += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
||||||
|
|
||||||
elif line_list_match:
|
elif line_list_match:
|
||||||
line_level = len(line_list_match.group(1)) / 2
|
line_level = len(line_list_match.group(1)) / 2
|
||||||
line_text = line_list_match.group(2)
|
line_text = line_list_match.group(2)
|
||||||
|
|
||||||
if line_level > prev_level:
|
if line_level > prev_level:
|
||||||
output += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
|
output[-1] += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
|
||||||
elif line_level < prev_level:
|
elif line_level < prev_level:
|
||||||
output += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
|
output[-1] += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
|
||||||
else:
|
else:
|
||||||
output += '<li>' + line_text + '</li>'
|
output[-1] += '<li>' + line_text + '</li>'
|
||||||
|
|
||||||
prev_level = line_level
|
prev_level = line_level
|
||||||
|
|
||||||
elif line.strip() == '' and prev_level:
|
elif line.strip() == '' and prev_level:
|
||||||
output += '</ul>' * (prev_level)
|
output[-1] += '</ul>' * (prev_level)
|
||||||
|
output.append('')
|
||||||
prev_level = 0
|
prev_level = 0
|
||||||
|
|
||||||
return output
|
if since_prev_release:
|
||||||
|
output.reverse()
|
||||||
|
|
||||||
|
return ''.join(output)
|
||||||
|
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)
|
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)
|
||||||
|
@@ -180,7 +180,7 @@ def process(opcode, data):
|
|||||||
info = json.loads(data)
|
info = json.loads(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(u"Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
|
logger.warn(u"Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
|
||||||
logger.debug(data)
|
logger.websocket_error(data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
info = info.get('NotificationContainer', info)
|
info = info.get('NotificationContainer', info)
|
||||||
|
@@ -2201,9 +2201,8 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
def get_sync(self, machine_id=None, user_id=None, **kwargs):
|
def get_sync(self, machine_id=None, user_id=None, **kwargs):
|
||||||
|
if user_id == 'null':
|
||||||
if not machine_id:
|
user_id = None
|
||||||
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
|
||||||
|
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id)
|
result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id)
|
||||||
@@ -2798,12 +2797,16 @@ class WebInterface(object):
|
|||||||
def get_server_update_params(self, **kwargs):
|
def get_server_update_params(self, **kwargs):
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
plexpass = plex_tv.get_plexpass_status()
|
plexpass = plex_tv.get_plexpass_status()
|
||||||
|
|
||||||
|
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
||||||
|
|
||||||
return {'plexpass': plexpass,
|
return {'plexpass': plexpass,
|
||||||
'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get(
|
'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get(
|
||||||
plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM),
|
plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM),
|
||||||
'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL,
|
'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL,
|
||||||
'pms_update_distro': plexpy.CONFIG.PMS_UPDATE_DISTRO,
|
'pms_update_distro': plexpy.CONFIG.PMS_UPDATE_DISTRO,
|
||||||
'pms_update_distro_build': plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD}
|
'pms_update_distro_build': plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD,
|
||||||
|
'plex_update_channel': 'plexpass' if update_channel == 'beta' else 'public'}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@@ -3126,8 +3129,7 @@ class WebInterface(object):
|
|||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def get_browser_notifications(self, **kwargs):
|
def get_browser_notifications(self, **kwargs):
|
||||||
browser = notifiers.BROWSER()
|
result = notifiers.get_browser_notifications()
|
||||||
result = browser.get_notifications()
|
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
notifications = result['notifications']
|
notifications = result['notifications']
|
||||||
@@ -3546,13 +3548,20 @@ class WebInterface(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def get_changelog(self, latest_only=False, update_shown=False, **kwargs):
|
def get_changelog(self, latest_only=False, since_prev_release=False, update_shown=False, **kwargs):
|
||||||
latest_only = True if latest_only == 'true' else False
|
latest_only = (latest_only == 'true')
|
||||||
|
since_prev_release = (since_prev_release == 'true')
|
||||||
|
|
||||||
|
if since_prev_release and plexpy.PREV_RELEASE == common.VERSION_NUMBER:
|
||||||
|
latest_only = True
|
||||||
|
since_prev_release = False
|
||||||
|
|
||||||
# Set update changelog shown status
|
# Set update changelog shown status
|
||||||
if update_shown == 'true':
|
if update_shown == 'true':
|
||||||
plexpy.CONFIG.__setattr__('UPDATE_SHOW_CHANGELOG', 0)
|
plexpy.CONFIG.__setattr__('UPDATE_SHOW_CHANGELOG', 0)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
return versioncheck.read_changelog(latest_only=latest_only)
|
|
||||||
|
return versioncheck.read_changelog(latest_only=latest_only, since_prev_release=since_prev_release)
|
||||||
|
|
||||||
##### Info #####
|
##### Info #####
|
||||||
|
|
||||||
@@ -4457,7 +4466,7 @@ class WebInterface(object):
|
|||||||
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||||
if s['location'] == 'lan':
|
if s['location'] == 'lan':
|
||||||
counts['lan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
counts['lan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||||
elif s['location'] == 'wan':
|
else:
|
||||||
counts['wan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
counts['wan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||||
|
|
||||||
result.update(counts)
|
result.update(counts)
|
||||||
|
Reference in New Issue
Block a user