Compare commits

...

36 Commits

Author SHA1 Message Date
JonnyWong16
e1e5a050c2 v2.0.15-beta 2018-01-27 11:08:45 -08:00
JonnyWong16
58996c1115 Unused now time 2018-01-27 10:59:48 -08:00
JonnyWong16
7301fe5f6e Remove 24 hour limit for recently added 2018-01-26 12:29:38 -08:00
JonnyWong16
a27c423569 Line up cards on the homepage 2018-01-24 21:37:02 -08:00
JonnyWong16
19680d3bc7 Refresh stream location on activity cards 2018-01-24 21:14:18 -08:00
JonnyWong16
ecaca4e5dc Change hover text from "View in" to "View on" 2018-01-24 21:07:12 -08:00
JonnyWong16
191de0b577 Add "View On" to Plex Web click-through 2018-01-24 21:04:34 -08:00
JonnyWong16
ebcc073b32 Add more server notification parameters. Rename plexpy parameters to tautulli. 2018-01-22 17:50:48 -08:00
JonnyWong16
043b3fd57b Update state for "Check server response" task 2018-01-22 13:44:51 -08:00
JonnyWong16
dd50502dcb Update Discord link to welcome channel 2018-01-22 11:27:04 -08:00
JonnyWong16
f159a1014d Don't add view_offset to live progress bar 2018-01-21 19:46:23 -08:00
JonnyWong16
abb801535c Add line break for Live progress 2018-01-21 16:09:48 -08:00
JonnyWong16
2732dbf1b1 Fix progress time for live tv 2018-01-21 16:07:32 -08:00
JonnyWong16
095d893005 Improve Live TV info on activity cards 2018-01-21 15:54:38 -08:00
JonnyWong16
5d8455d141 Get rating key for live sessions from websocket data 2018-01-21 13:09:02 -08:00
JonnyWong16
aa3450bfcc Add Labels and Collections to notification parameters 2018-01-20 20:01:01 -08:00
JonnyWong16
45c2ccdffe v2.0.14-beta 2018-01-20 11:42:36 -08:00
JonnyWong16
fc14c3165f Remove email line break message 2018-01-20 11:30:11 -08:00
JonnyWong16
0fad245148 Try to cleanly shutdown loggers 2018-01-20 11:27:58 -08:00
JonnyWong16
79609c384e Show all changelogs when updated since previous version 2018-01-20 10:27:08 -08:00
JonnyWong16
09054ddb4b Correct clear logs message 2018-01-19 19:11:55 -08:00
JonnyWong16
6f912d4aa2 Add date header to Emails and do not add HTML line breaks automatically 2018-01-19 15:54:58 -08:00
JonnyWong16
96033a8214 Rename Tautulli update notification parameters 2018-01-19 14:59:00 -08:00
JonnyWong16
5ca65f4797 Catch json ValueError in metadata cache 2018-01-19 07:13:53 -08:00
JonnyWong16
d2fccbde68 Json dump custom conditions 2018-01-18 14:02:47 -08:00
JonnyWong16
e6b48d7baf Check for browser proxy compatibility 2018-01-17 21:02:31 -08:00
JonnyWong16
3e51310511 Re-enable browser notifications 2018-01-17 17:01:44 -08:00
JonnyWong16
32b43202c2 Attempt at fixing stuck sessions which require flishing the database 2018-01-15 18:55:37 -08:00
JonnyWong16
446170f8de Reduce websocket logging to playing and timeline only 2018-01-15 17:59:12 -08:00
JonnyWong16
c5a9ecd4ac Make sure websocket events are for library items 2018-01-15 14:49:49 -08:00
JonnyWong16
2af5f817a3 Plex Web url for tracks should go to the album page 2018-01-15 14:37:40 -08:00
JonnyWong16
4e55cf3cd4 Add all other bandwidth to WAN 2018-01-15 14:25:42 -08:00
JonnyWong16
eeb0478813 Use font-awesome arrow on activity cards 2018-01-14 20:45:27 -08:00
JonnyWong16
33739f1cb2 Fix check activity session write success 2018-01-13 21:16:07 -08:00
JonnyWong16
515e6a8071 Sort selectize when rendered 2018-01-13 17:47:24 -08:00
JonnyWong16
2b22f8eb4f Add select/remove all options for emails 2018-01-13 17:18:09 -08:00
27 changed files with 493 additions and 237 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@
*.db* *.db*
*.db-journal *.db-journal
*.ini *.ini
release.lock
version.lock version.lock
logs/* logs/*
cache/* cache/*

View File

@@ -1,5 +1,34 @@
# Changelog # Changelog
## 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:

View File

@@ -1,6 +1,6 @@
# Tautulli # Tautulli
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/36ggawe) [![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/tQcWEUp)
[![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/) [![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/)
[![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) [![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](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:

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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,

View File

@@ -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()} &rarr; ${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'])} &rarr; ${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()} &rarr; ${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()} &rarr; ${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>&nbsp;
</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>&nbsp; <i class="fa fa-fw fa-film"></i>&nbsp;
@@ -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>&nbsp; <i class="fa fa-fw fa-cloud"></i>&nbsp;
</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':

View File

@@ -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>
@@ -321,7 +322,7 @@
$('#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 +396,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() + ' &rarr; ' + 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 +429,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 + ' &rarr; ' + 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 +445,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]) + ' &rarr; ' + 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 +457,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() + ' &rarr; ' + 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 +485,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 +512,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 +820,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,

View File

@@ -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);">

View File

@@ -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 () {

View File

@@ -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({

View File

@@ -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;

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
@@ -121,13 +125,19 @@ class ActivityHandler(object):
# 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)
if row_id:
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
# Remove the session from our temp session table # Remove the session from our temp session table
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue" logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
% (str(self.get_session_key()), str(self.get_rating_key()))) % (str(self.get_session_key()), str(self.get_rating_key())))
ap.delete_session(session_key=self.get_session_key()) ap.delete_session(row_id=row_id)
delete_metadata_cache(self.get_session_key()) 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():
@@ -245,9 +255,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()
@@ -324,6 +331,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 +340,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 \
@@ -433,13 +445,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 databaase 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 +464,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), function=force_stop_stream,
args=[session_key], seconds=30) args=[session_key], seconds=30)
else: else:
@@ -498,12 +511,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:

View File

@@ -160,14 +160,13 @@ def check_active_sessions(ws_request=False):
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_stop'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream, '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:

View File

@@ -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'])
@@ -284,7 +290,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']]
@@ -414,8 +420,8 @@ class ActivityProcessor(object):
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_metadata transaction...") # logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_metadata transaction...")
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 +462,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():

View File

@@ -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,15 +279,21 @@ 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.'},
] ]
}, },
{ {
@@ -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.'},
] ]
}, },
] ]

View File

@@ -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

View File

@@ -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,34 +839,26 @@ 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
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'],
@@ -882,12 +873,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

View File

@@ -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 (&lt;br&gt;) 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}

View File

@@ -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:
@@ -588,6 +588,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 +610,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 +651,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 +692,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 +735,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 +779,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 +821,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 +864,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 +908,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 +950,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 +993,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 +1039,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 +1080,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 +1178,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))
@@ -1586,6 +1602,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 +1641,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 +1651,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
@@ -1699,6 +1718,21 @@ 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']) stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])

View File

@@ -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)

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.13-beta" PLEXPY_RELEASE_VERSION = "v2.0.15-beta"

View File

@@ -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)

View File

@@ -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)

View File

@@ -3126,8 +3126,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 +3545,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 +4463,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)