Compare commits

...

35 Commits

Author SHA1 Message Date
JonnyWong16
5d2219f2f8 v2.0.17-beta 2018-02-03 09:35:05 -08:00
JonnyWong16
56dc28eed3 Clear session metadata cache on startup 2018-02-03 09:06:05 -08:00
JonnyWong16
3e723d4373 Fix photo album media type 2018-02-02 23:49:38 -08:00
JonnyWong16
f5e341e655 Don't sanitize tags for Slack and Discord 2018-02-02 23:22:41 -08:00
JonnyWong16
3c81100957 Fix media info table sorting 2018-02-02 23:03:48 -08:00
JonnyWong16
304378f93b Add Zapier notification agent 2018-02-01 22:11:33 -08:00
JonnyWong16
de6b6e8124 Check for any falsy value in sync item filters 2018-01-31 08:59:37 -08:00
JonnyWong16
d15223fb1a v2.0.16-beta 2018-01-30 23:20:34 -08:00
JonnyWong16
d29a12b6db Add user filter to the synced table 2018-01-30 23:07:21 -08:00
JonnyWong16
9100e25a21 Pass copy of notification data to prevent multithreading issues 2018-01-30 23:04:44 -08:00
JonnyWong16
7672f1955e Fix sync table not loading 2018-01-30 21:19:37 -08:00
JonnyWong16
5f52171fc4 Add "Use Server Setting" as Plex update channel 2018-01-30 19:56:48 -08:00
JonnyWong16
31ac82ad71 Comment out logging for writing session history to database 2018-01-30 19:06:10 -08:00
JonnyWong16
38ca4e37a6 Fix matching of synced playback 2018-01-30 19:04:30 -08:00
JonnyWong16
3c55550702 Add logging for writing session history to database 2018-01-30 10:04:28 -08:00
JonnyWong16
7dff6b121b Log force stopped message 2018-01-30 09:31:13 -08:00
JonnyWong16
d77d889695 Fix activity callback function argument 2018-01-30 09:13:06 -08:00
JonnyWong16
318a21438f Fix sometimes time showing as "0:60" 2018-01-28 20:19:49 -08:00
JonnyWong16
7175b57a28 Fix "unknown" stream resolution in graphs 2018-01-28 10:06:47 -08:00
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
29 changed files with 641 additions and 267 deletions

View File

@@ -1,5 +1,41 @@
# Changelog # Changelog
## v2.0.17-beta (2018-02-03)
* Notifications:
* Fix: Unable to use @ mentions tags for Discord and Slack.
* New: Added Zapier notification agent.
* API:
* Fix: get_synced_items returning no results.
* Fix: get_library_media_info returning incorrect media type for photo albums.
* Fix: get_library_media_info not being able to sort by title.
## v2.0.16-beta (2018-01-30)
* Monitoring:
* Fix: Timestamp sometimes showing as "0:60" on the activity cards.
* Fix: Incorrect session information being shown for playback of synced content.
* Fix: Sessions not being stopped when "Playback Stopped" notifications were enabled.
* UI:
* Fix: Stream resolution showing up as "unknown" on the graphs.
* New: Added user filter to the Synced Items table.
* Other:
* New: Option to use the Plex server update channel when checking for updates.
## v2.0.15-beta (2018-01-27)
* Monitoring:
* Fix: Live TV sessions not being stopped in History.
* Fix: Stream location showing as "unknown" on the activity cards.
* New: Improved Live TV details on the activity cards.
* Notifications:
* New: Added labels and collections to notification parameters.
* New: Added more server details to notification parameters.
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
## v2.0.14-beta (2018-01-20) ## v2.0.14-beta (2018-01-20)
* Monitoring: * Monitoring:

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

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

@@ -706,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;
@@ -1124,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;
@@ -1759,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,7 +201,7 @@ 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()} <i class="fa fa-long-arrow-right"></i> ${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()})
@@ -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'])} <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'])}) 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()} <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()}) 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()})
@@ -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

@@ -114,7 +114,7 @@
$.ajax({ $.ajax({
url: 'get_user_names', url: 'get_user_names',
type: 'get', type: 'get',
dataType: "json", dataType: 'json',
success: function (data) { success: function (data) {
var select = $('#history-user'); var select = $('#history-user');
data.sort(function (a, b) { data.sort(function (a, b) {
@@ -130,7 +130,6 @@
function loadHistoryTable(media_type, selected_user_id) { function loadHistoryTable(media_type, selected_user_id) {
history_table_options.ajax = { history_table_options.ajax = {
url: 'get_history', url: 'get_history',
type: 'post',
data: function (d) { data: function (d) {
return { return {
json_data: JSON.stringify(d), json_data: JSON.stringify(d),
@@ -138,9 +137,13 @@
user_id: selected_user_id user_id: selected_user_id
}; };
} }
} };
history_table = $('#history_table').DataTable(history_table_options); history_table = $('#history_table').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); var colvis = new $.fn.dataTable.ColVis(history_table, {
buttonText: '<i class="fa fa-columns"></i> Select columns',
buttonClass: 'btn btn-dark',
exclude: [0, 11]
});
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table); clearSearchButton('history_table', history_table);
@@ -160,7 +163,7 @@
} }
var media_type = null; var media_type = null;
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}";
loadHistoryTable(media_type, selected_user_id); loadHistoryTable(media_type, selected_user_id);
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':

View File

@@ -309,14 +309,17 @@
streams_header = streams_header.replace(/, $/, '') + ')'; streams_header = streams_header.replace(/, $/, '') + ')';
$('#currentActivityHeader-streams').text(streams_header); $('#currentActivityHeader-streams').text(streams_header);
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps')) + ' ('; var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps'));
var lan_wan_bandwidth_header = '';
if (lan_bw) { if (lan_bw) {
bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', '; lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
} }
if (wan_bw) { if (wan_bw) {
bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', '; lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
}
if (lan_wan_bandwidth_header) {
bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')';
} }
bandwidth_header = bandwidth_header.replace(/, $/, '') + ')';
$('#currentActivityHeader-bandwidth').text(bandwidth_header); $('#currentActivityHeader-bandwidth').text(bandwidth_header);
$('#currentActivityHeader').show(); $('#currentActivityHeader').show();
@@ -485,6 +488,8 @@
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')'); $('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
$('#synced_quality_profile-' + key).html(s.synced_quality_profile); $('#synced_quality_profile-' + key).html(s.synced_quality_profile);
$('#location-' + key).html(s.location.toUpperCase());
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) { if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
var bw = parseInt(s.bandwidth); var bw = parseInt(s.bandwidth);
if (bw !== "Unknown") { if (bw !== "Unknown") {
@@ -510,7 +515,7 @@
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%'); .attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
var progress_bar = $('#progress-bar-' + key); var progress_bar = $('#progress-bar-' + key);
progress_bar.data('state', s.state); progress_bar.data('state', s.state);
if (progress_bar.data('last_view_offset') !== s.view_offset) { if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
} }

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

@@ -290,19 +290,9 @@ String.prototype.toProperCase = function () {
function millisecondsToMinutes(ms, roundToMinute) { function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) { if (ms > 0) {
seconds = ms / 1000; var minutes = Math.floor(ms / 60000);
minutes = seconds / 60; var seconds = ((ms % 60000) / 1000).toFixed(0);
if (roundToMinute) { return (seconds == 60 ? (minutes+1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
output = Math.round(minutes, 0)
} else {
minutesFloor = Math.floor(minutes);
secondsReal = Math.round((seconds - (minutesFloor * 60)), 0);
if (secondsReal < 10) {
secondsReal = '0' + secondsReal;
}
output = minutesFloor + ':' + secondsReal;
}
return output;
} else { } else {
if (roundToMinute) { if (roundToMinute) {
return '0'; return '0';

View File

@@ -21,7 +21,7 @@ history_table_options = {
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>", "infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
"emptyTable": "No data in table", "emptyTable": "No data in table",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>' "loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
}, },
"pagingType": "full_numbers", "pagingType": "full_numbers",
"stateSave": true, "stateSave": true,
"processing": false, "processing": false,
@@ -172,7 +172,7 @@ history_table_options = {
}, },
"width": "33%", "width": "33%",
"className": "datatable-wrap" "className": "datatable-wrap"
}, },
{ {
"targets": [7], "targets": [7],
"data":"started", "data":"started",
@@ -322,7 +322,7 @@ history_table_options = {
$(row).addClass('current-activity-row'); $(row).addClass('current-activity-row');
} }
} }
} };
// Parent table platform modal // Parent table platform modal
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () { $('.history_table').on('click', '> tbody > tr > td.modal-control', function () {

View File

@@ -98,7 +98,7 @@ sync_table_options = {
"data": "total_size", "data": "total_size",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData > 0 ) { if (cellData > 0 ) {
megabytes = Math.round((cellData/1024)/1024, 0) megabytes = Math.round((cellData/1024)/1024, 0);
$(td).html(megabytes + 'MB'); $(td).html(megabytes + 'MB');
} else { } else {
$(td).html('0MB'); $(td).html('0MB');
@@ -144,14 +144,16 @@ sync_table_options = {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows..."; var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0) showMsg(msg, false, false, 0)
} }
} };
$('#sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () { $('#sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
var tr = $(this).parents('tr'); var tr = $(this).parents('tr');
var row = sync_table.row(tr); var row = sync_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var index_delete = syncs_to_delete.findIndex(x => x.client_id == rowData['client_id'] && x.sync_id == rowData['sync_id']); var index_delete = syncs_to_delete.findIndex(function (x) {
return x.client_id === rowData['client_id'] && x.sync_id === rowData['sync_id'];
});
if (index_delete === -1) { if (index_delete === -1) {
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] }); syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });

View File

@@ -580,6 +580,18 @@
}); });
var join_device_names = $join_device_names[0].selectize; var join_device_names = $join_device_names[0].selectize;
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});
% elif notifier['agent_name'] == 'zapier':
$('#zapier_test_hook').click(function () {
$.get('zapier_test_hook', { 'zapier_hook': $('#zapier_hook').val() }, function (data) {
if (data.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + data.msg, false, true, 5000);
} else {
showMsg('<i class="fa fa-times"></i> ' + data.msg, false, true, 5000, true);
}
});
});
% endif % endif
function validateLogic() { function validateLogic() {

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

@@ -553,9 +553,10 @@
<div id="pms_update_options"> <div id="pms_update_options">
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-3">
<label for="pms_update_channel">Update Channel</label> <label for="pms_update_channel">Update Channel</label>
<select class="form-control" id="pms_update_channel" name="pms_update_channel"> <select class="form-control" id="pms_update_channel" name="pms_update_channel">
<option value="plex">Use Server Setting</option>
<option value="public">Public</option> <option value="public">Public</option>
</select> </select>
</div> </div>
@@ -2104,32 +2105,41 @@ $(document).ready(function() {
var update_channel = update_params.pms_update_channel; var update_channel = update_params.pms_update_channel;
var update_distro = update_params.pms_update_distro; var update_distro = update_params.pms_update_distro;
var update_distro_build = update_params.pms_update_distro_build; var update_distro_build = update_params.pms_update_distro_build;
var plex_update_channel = update_params.plex_update_channel;
$("#pms_update_channel option[value='plexpass']").remove(); $('#pms_update_channel option[value=beta]').remove();
if (plexpass) { if (plexpass) {
var selected = (update_channel == 'plexpass') ? true : false; var selected = (update_channel == 'beta') ? true : false;
$('#pms_update_channel') $('#pms_update_channel')
.append($('<option></option>') .append($('<option></option>')
.text('Plex Pass') .text('Beta')
.val('plexpass') .val('beta')
.prop('selected', selected)); .prop('selected', selected));
} }
$.getJSON('https://plex.tv/api/downloads/1.json?channel=' + update_channel, function (downloads) { $.ajax({
platform_downloads = downloads.computer[platform] || downloads.nas[platform]; url: 'https://plex.tv/api/downloads/1.json?channel=' + plex_update_channel,
if (platform_downloads) { type: 'GET',
$("#pms_update_distro_build option").remove(); dataType: 'json',
$.each(platform_downloads.releases, function (index, item) { beforeSend: function (xhr) {
var label = (platform_downloads.releases.length == 1) ? platform_downloads.name : platform_downloads.name + ' - ' + item.label; xhr.setRequestHeader('X-Plex-Token', $('#pms_token').val());
var selected = (item.distro == update_distro && item.build == update_distro_build) ? true : false; },
$('#pms_update_distro_build') success: function (downloads) {
.append($('<option></option>') var platform_downloads = downloads.computer[platform] || downloads.nas[platform];
.text(label) if (platform_downloads) {
.val(item.build) $("#pms_update_distro_build option").remove();
.attr('data-distro', item.distro) $.each(platform_downloads.releases, function (index, item) {
.prop('selected', selected)); var label = (platform_downloads.releases.length === 1) ? platform_downloads.name : platform_downloads.name + ' - ' + item.label;
}) var selected = (item.distro === update_distro && item.build === update_distro_build) ? true : false;
$('#pms_update_distro').val($("#pms_update_distro_build option:selected").data('distro')) $('#pms_update_distro_build')
.append($('<option></option>')
.text(label)
.val(item.build)
.attr('data-distro', item.distro)
.prop('selected', selected));
});
$('#pms_update_distro').val($('#pms_update_distro_build option:selected').data('distro'))
}
} }
}); });
}); });

View File

@@ -27,6 +27,16 @@
</button>&nbsp </button>&nbsp
</div> </div>
% endif % endif
% if _session['user_group'] == 'admin':
<div class="btn-group" id="user-selection">
<label>
<select name="sync-user" id="sync-user" class="btn" style="color: inherit;">
<option value="">All Users</option>
<option disabled>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</option>
</select>
</label>
</div>
% endif
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button> <button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
</div> </div>
@@ -87,17 +97,45 @@
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script> <script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
sync_table_options.ajax = { // Load user ids and names (for the selector)
url: 'get_sync', $.ajax({
data: function (d) { url: 'get_user_names',
d.user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}" type: 'get',
dataType: 'json',
success: function (data) {
var select = $('#sync-user');
data.sort(function (a, b) {
return a.friendly_name.localeCompare(b.friendly_name);
});
data.forEach(function (item) {
select.append('<option value="' + item.user_id + '">' +
item.friendly_name + '</option>');
});
} }
} });
sync_table = $('#sync_table').DataTable(sync_table_options);
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0] } );
$( colvis.button() ).appendTo('div.colvis-button-bar');
clearSearchButton('sync_table', sync_table); function loadSyncTable(selected_user_id) {
sync_table_options.ajax = {
url: 'get_sync?user_id=' + selected_user_id
};
sync_table = $('#sync_table').DataTable(sync_table_options);
var colvis = new $.fn.dataTable.ColVis(sync_table, {
buttonText: '<i class="fa fa-columns"></i> Select columns',
buttonClass: 'btn btn-dark',
exclude: [0]
});
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('sync_table', sync_table);
$('#sync-user').on('change', function () {
selected_user_id = $(this).val() || null;
sync_table.ajax.url('get_sync?user_id=' + selected_user_id).load();
});
}
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}";
loadSyncTable(selected_user_id);
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
$('#row-edit-mode').on('click', function() { $('#row-edit-mode').on('click', function() {

View File

@@ -15,6 +15,7 @@
import os import os
from Queue import Queue from Queue import Queue
import shutil
import sqlite3 import sqlite3
import sys import sys
import subprocess import subprocess
@@ -156,6 +157,16 @@ def initialize(config_file):
except OSError as e: except OSError as e:
logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e)) logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e))
if CONFIG.CACHE_DIR:
session_metadata_folder = os.path.join(CONFIG.CACHE_DIR, 'session_metadata')
try:
shutil.rmtree(session_metadata_folder, ignore_errors=True)
except OSError as e:
pass
if not os.path.exists(session_metadata_folder):
os.mkdir(session_metadata_folder)
# Initialize the database # Initialize the database
logger.info(u"Checking if the database upgrades are required...") logger.info(u"Checking if the database upgrades are required...")
try: try:
@@ -382,7 +393,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:
@@ -404,7 +415,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

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
@@ -93,14 +97,15 @@ class ActivityHandler(object):
% (str(session['session_key']), str(session['user_id']), session['username'], % (str(session['session_key']), str(session['user_id']), session['username'],
str(session['rating_key']), session['full_title'])) str(session['rating_key']), session['full_title']))
plexpy.NOTIFY_QUEUE.put({'stream_data': session, 'notify_action': 'on_play'}) plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
# Write the new session to our temp session table # Write the new session to our temp session table
self.update_db_session(session=session) self.update_db_session(session=session)
def on_stop(self, force_stop=False): def on_stop(self, force_stop=False):
if self.is_valid_session(): if self.is_valid_session():
logger.debug(u"Tautulli ActivityHandler :: Session %s stopped." % str(self.get_session_key())) logger.debug(u"Tautulli ActivityHandler :: Session %s %sstopped."
% (str(self.get_session_key()), 'force ' if force_stop else ''))
# Set the session last_paused timestamp # Set the session last_paused timestamp
ap = activity_processor.ActivityProcessor() ap = activity_processor.ActivityProcessor()
@@ -117,7 +122,7 @@ class ActivityHandler(object):
# Retrieve the session data from our temp table # Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key()) db_session = ap.get_session_by_key(session_key=self.get_session_key())
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_stop'}) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
# Write it to the history table # Write it to the history table
monitor_proc = activity_processor.ActivityProcessor() monitor_proc = activity_processor.ActivityProcessor()
@@ -154,7 +159,7 @@ class ActivityHandler(object):
db_session = ap.get_session_by_key(session_key=self.get_session_key()) db_session = ap.get_session_by_key(session_key=self.get_session_key())
if not still_paused: if not still_paused:
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_pause'}) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
def on_resume(self): def on_resume(self):
if self.is_valid_session(): if self.is_valid_session():
@@ -173,7 +178,7 @@ class ActivityHandler(object):
# Retrieve the session data from our temp table # Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key()) db_session = ap.get_session_by_key(session_key=self.get_session_key())
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_resume'}) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
def on_buffer(self): def on_buffer(self):
if self.is_valid_session(): if self.is_valid_session():
@@ -211,7 +216,7 @@ class ActivityHandler(object):
# Retrieve the session data from our temp table # Retrieve the session data from our temp table
db_session = ap.get_session_by_key(session_key=self.get_session_key()) db_session = ap.get_session_by_key(session_key=self.get_session_key())
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_buffer'}) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
# This function receives events from our websocket connection # This function receives events from our websocket connection
def process(self): def process(self):
@@ -226,7 +231,7 @@ class ActivityHandler(object):
if db_session: if db_session:
# Re-schedule the callback to reset the 5 minutes timer # Re-schedule the callback to reset the 5 minutes timer
schedule_callback('session_key-{}'.format(self.get_session_key()), schedule_callback('session_key-{}'.format(self.get_session_key()),
function=force_stop_stream, args=[self.get_session_key()], minutes=5) func=force_stop_stream, args=[self.get_session_key()], minutes=5)
last_state = db_session['state'] last_state = db_session['state']
last_key = str(db_session['rating_key']) last_key = str(db_session['rating_key'])
@@ -274,7 +279,7 @@ class ActivityHandler(object):
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \ db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states): and not any(d['notify_action'] == 'on_watched' for d in notify_states):
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session, 'notify_action': 'on_watched'}) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_watched'})
else: else:
# We don't have this session in our table yet, start a new one. # We don't have this session in our table yet, start a new one.
@@ -283,7 +288,7 @@ class ActivityHandler(object):
# Schedule a callback to force stop a stale stream 5 minutes later # Schedule a callback to force stop a stale stream 5 minutes later
schedule_callback('session_key-{}'.format(self.get_session_key()), schedule_callback('session_key-{}'.format(self.get_session_key()),
function=force_stop_stream, args=[self.get_session_key()], minutes=5) func=force_stop_stream, args=[self.get_session_key()], minutes=5)
class TimelineHandler(object): class TimelineHandler(object):
@@ -366,7 +371,7 @@ class TimelineHandler(object):
% (title, str(rating_key), str(grandparent_rating_key))) % (title, str(rating_key), str(grandparent_rating_key)))
# Schedule a callback to clear the recently added queue # Schedule a callback to clear the recently added queue
schedule_callback('rating_key-{}'.format(grandparent_rating_key), function=clear_recently_added_queue, schedule_callback('rating_key-{}'.format(grandparent_rating_key), func=clear_recently_added_queue,
args=[grandparent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY) args=[grandparent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
elif media_type in ('season', 'album'): elif media_type in ('season', 'album'):
@@ -382,7 +387,7 @@ class TimelineHandler(object):
% (title, str(rating_key), str(parent_rating_key))) % (title, str(rating_key), str(parent_rating_key)))
# Schedule a callback to clear the recently added queue # Schedule a callback to clear the recently added queue
schedule_callback('rating_key-{}'.format(parent_rating_key), function=clear_recently_added_queue, schedule_callback('rating_key-{}'.format(parent_rating_key), func=clear_recently_added_queue,
args=[parent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY) args=[parent_rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
else: else:
@@ -393,7 +398,7 @@ class TimelineHandler(object):
% (title, str(rating_key))) % (title, str(rating_key)))
# Schedule a callback to clear the recently added queue # Schedule a callback to clear the recently added queue
schedule_callback('rating_key-{}'.format(rating_key), function=clear_recently_added_queue, schedule_callback('rating_key-{}'.format(rating_key), func=clear_recently_added_queue,
args=[rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY) args=[rating_key], seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
# A movie, show, or artist is done processing # A movie, show, or artist is done processing
@@ -423,7 +428,7 @@ def del_keys(key):
del_keys(RECENTLY_ADDED_QUEUE.pop(key)) del_keys(RECENTLY_ADDED_QUEUE.pop(key))
def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs): def schedule_callback(id, func=None, remove_job=False, args=None, **kwargs):
if ACTIVITY_SCHED.get_job(id): if ACTIVITY_SCHED.get_job(id):
if remove_job: if remove_job:
ACTIVITY_SCHED.remove_job(id) ACTIVITY_SCHED.remove_job(id)
@@ -433,7 +438,7 @@ def schedule_callback(id, function=None, remove_job=False, args=None, **kwargs):
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs))) run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
elif not remove_job: elif not remove_job:
ACTIVITY_SCHED.add_job( ACTIVITY_SCHED.add_job(
function, args=args, id=id, trigger=DateTrigger( func, args=args, id=id, trigger=DateTrigger(
run_date=datetime.datetime.now() + datetime.timedelta(**kwargs))) run_date=datetime.datetime.now() + datetime.timedelta(**kwargs)))
@@ -444,7 +449,7 @@ def force_stop_stream(session_key):
row_id = ap.write_session_history(session=session) row_id = ap.write_session_history(session=session)
if row_id: if row_id:
# If session is written to the databaase successfully, remove the session from the session table # If session is written to the database successfully, remove the session from the session table
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (session['session_key'], session['rating_key'])) % (session['session_key'], session['rating_key']))
ap.delete_session(row_id=row_id) ap.delete_session(row_id=row_id)
@@ -460,7 +465,7 @@ def force_stop_stream(session_key):
ap.increment_write_attempts(session_key=session_key) ap.increment_write_attempts(session_key=session_key)
# Reschedule for 30 seconds later # Reschedule for 30 seconds later
schedule_callback('session_key-{}'.format(session_key), function=force_stop_stream, schedule_callback('session_key-{}'.format(session_key), func=force_stop_stream,
args=[session_key], seconds=30) args=[session_key], seconds=30)
else: else:
@@ -507,12 +512,12 @@ def on_created(rating_key, **kwargs):
if metadata: if metadata:
notify = True notify = True
now = int(time.time()) # now = int(time.time())
#
if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago # if helpers.cast_to_int(metadata['added_at']) < now - 86400: # Updated more than 24 hours ago
logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying." # logger.debug(u"Tautulli TimelineHandler :: Library item %s added more than 24 hours ago. Not notifying."
% str(rating_key)) # % str(rating_key))
notify = False # notify = False
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
if 'child_keys' not in kwargs: if 'child_keys' not in kwargs:
@@ -541,7 +546,7 @@ def on_created(rating_key, **kwargs):
def delete_metadata_cache(session_key): def delete_metadata_cache(session_key):
try: try:
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % session_key)) os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % session_key))
except IOError as e: except IOError as e:
logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s" logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
% (session_key, e)) % (session_key, e))

View File

@@ -61,12 +61,12 @@ def check_active_sessions(ws_request=False):
if session['state'] == 'paused': if session['state'] == 'paused':
logger.debug(u"Tautulli Monitor :: Session %s paused." % stream['session_key']) logger.debug(u"Tautulli Monitor :: Session %s paused." % stream['session_key'])
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_pause'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
if session['state'] == 'playing' and stream['state'] == 'paused': if session['state'] == 'playing' and stream['state'] == 'paused':
logger.debug(u"Tautulli Monitor :: Session %s resumed." % stream['session_key']) logger.debug(u"Tautulli Monitor :: Session %s resumed." % stream['session_key'])
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_resume'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
if stream['state'] == 'paused' and not ws_request: if stream['state'] == 'paused' and not ws_request:
# The stream is still paused so we need to increment the paused_counter # The stream is still paused so we need to increment the paused_counter
@@ -104,7 +104,7 @@ def check_active_sessions(ws_request=False):
'WHERE session_key = ? AND rating_key = ?', 'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']]) [stream['session_key'], stream['rating_key']])
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_buffer'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
else: else:
# Subsequent buffer notifications after wait time # Subsequent buffer notifications after wait time
@@ -118,7 +118,7 @@ def check_active_sessions(ws_request=False):
'WHERE session_key = ? AND rating_key = ?', 'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']]) [stream['session_key'], stream['rating_key']])
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_buffer'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
logger.debug(u"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s." logger.debug(u"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
% (stream['session_key'], % (stream['session_key'],
@@ -135,7 +135,7 @@ def check_active_sessions(ws_request=False):
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \ session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states): and not any(d['notify_action'] == 'on_watched' for d in notify_states):
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_watched'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
else: else:
# The user has stopped playing a stream # The user has stopped playing a stream
@@ -155,9 +155,9 @@ def check_active_sessions(ws_request=False):
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \ stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states): and not any(d['notify_action'] == 'on_watched' for d in notify_states):
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_watched'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_stop'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
# Write the item history on playback stop # Write the item history on playback stop
row_id = monitor_process.write_session_history(session=stream) row_id = monitor_process.write_session_history(session=stream)
@@ -243,7 +243,7 @@ def check_recently_added():
if 0 < time_threshold - int(item['added_at']) <= time_interval: if 0 < time_threshold - int(item['added_at']) <= time_interval:
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key'])) logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'}) plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
else: else:
item = max(metadata, key=lambda x:x['added_at']) item = max(metadata, key=lambda x:x['added_at'])
@@ -261,7 +261,7 @@ def check_recently_added():
logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key'])) logger.debug(u"Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
# Check if any notification agents have notifications enabled # Check if any notification agents have notifications enabled
plexpy.NOTIFY_QUEUE.put({'timeline_data': item, 'notify_action': 'on_created'}) plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
def check_server_response(): def check_server_response():

View File

@@ -127,7 +127,7 @@ class ActivityProcessor(object):
if result == 'insert': if result == 'insert':
# Check if any notification agents have notifications enabled # Check if any notification agents have notifications enabled
if notify: if notify:
plexpy.NOTIFY_QUEUE.put({'stream_data': values, 'notify_action': 'on_play'}) plexpy.NOTIFY_QUEUE.put({'stream_data': values.copy(), 'notify_action': 'on_play'})
# If it's our first write then time stamp it. # If it's our first write then time stamp it.
started = int(time.time()) started = int(time.time())
@@ -235,7 +235,8 @@ class ActivityProcessor(object):
## TODO: Fix media info from imports. Temporary media info from import session. ## TODO: Fix media info from imports. Temporary media info from import session.
media_info = session media_info = session
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history table...") # logger.debug(u"Tautulli ActivityProcessor :: Attempting to write sessionKey %s to session_history table..."
# % session['session_key'])
keys = {'id': None} keys = {'id': None}
values = {'started': session['started'], values = {'started': session['started'],
'stopped': stopped, 'stopped': stopped,
@@ -260,7 +261,8 @@ class ActivityProcessor(object):
'view_offset': session['view_offset'] 'view_offset': session['view_offset']
} }
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history transaction...") # logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history transaction..."
# % session['session_key'])
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values) self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
# Check if we should group the session, select the last two rows from the user # Check if we should group the session, select the last two rows from the user
@@ -304,7 +306,8 @@ class ActivityProcessor(object):
# Write the session_history_media_info table # Write the session_history_media_info table
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history_media_info table...") # logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_media_info table..."
# % session['session_key'])
keys = {'id': last_id} keys = {'id': last_id}
values = {'rating_key': session['rating_key'], values = {'rating_key': session['rating_key'],
'video_decision': session['video_decision'], 'video_decision': session['video_decision'],
@@ -371,7 +374,8 @@ class ActivityProcessor(object):
'optimized_version_title': session['optimized_version_title'] 'optimized_version_title': session['optimized_version_title']
} }
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_media_info transaction...") # logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_media_info transaction..."
# % session['session_key'])
self.db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values) self.db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values)
# Write the session_history_metadata table # Write the session_history_metadata table
@@ -381,7 +385,8 @@ class ActivityProcessor(object):
genres = ";".join(metadata['genres']) genres = ";".join(metadata['genres'])
labels = ";".join(metadata['labels']) labels = ";".join(metadata['labels'])
# logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to session_history_metadata table...") # logger.debug(u"Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_metadata table..."
# % session['session_key'])
keys = {'id': last_id} keys = {'id': last_id}
values = {'rating_key': session['rating_key'], values = {'rating_key': session['rating_key'],
'parent_rating_key': session['parent_rating_key'], 'parent_rating_key': session['parent_rating_key'],
@@ -417,7 +422,8 @@ class ActivityProcessor(object):
'labels': labels 'labels': labels
} }
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_metadata transaction...") # logger.debug(u"Tautulli ActivityProcessor :: Writing sessionKey %s session_history_metadata transaction..."
# % session['session_key'])
self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values) self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)
# Return the session row id when the session is successfully written to the database # Return the session row id when the session is successfully written to the database

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,16 +279,22 @@ NOTIFICATION_PARAMETERS = [
{ {
'category': 'Global', 'category': 'Global',
'parameters': [ 'parameters': [
{'name': 'Tautulli Version', 'type': 'str', 'value': 'plexpy_version', 'description': 'The current version of Tautulli.'}, {'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', 'description': 'The current version of Tautulli.'},
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'plexpy_branch', 'description': 'The current git branch of Tautulli.'}, {'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', 'description': 'The current git remote of Tautulli.'},
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'plexpy_commit', 'description': 'The current git commit hash of Tautulli.'}, {'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', 'description': 'The current git branch of Tautulli.'},
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', 'description': 'The current git commit hash of Tautulli.'},
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'}, {'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
{'name': 'Server Uptime', 'type': 'str', 'value': 'server_uptime', 'description': 'The uptime (in days, hours, mins, secs) of your Plex Server.'}, {'name': 'Server IP', 'type': 'str', 'value': 'server_ip', 'description': 'The connection IP address for your Plex Server.'},
{'name': 'Server Port', 'type': 'int', 'value': 'server_port', 'description': 'The connection port for your Plex Server.'},
{'name': 'Server URL', 'type': 'str', 'value': 'server_url', 'description': 'The connection URL for your Plex Server.'},
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', 'description': 'The platform of your Plex Server.'},
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'}, {'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'}, {'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
{'name': 'Datestamp', 'type': 'int', 'value': 'datestamp', 'description': 'The date (in date format) the notification was triggered.'}, {'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification was triggered.'},
{'name': 'Timestamp', 'type': 'int', 'value': 'timestamp', 'description': 'The time (in time format) the notification was triggered.'}, {'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification was triggered.'},
] {'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification was triggered.'},
]
}, },
{ {
'category': 'Stream Details', 'category': 'Stream Details',
@@ -394,10 +400,12 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'}, {'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'}, {'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'}, {'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
{'name': 'Director', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'}, {'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
{'name': 'Writer', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'}, {'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
{'name': 'Actor', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'}, {'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
{'name': 'Genre', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'}, {'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
{'name': 'Collections', 'type': 'str', 'value': 'collections', 'description': 'A list of collections for the item.'},
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'}, {'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'}, {'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'}, {'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},

View File

@@ -61,7 +61,7 @@ _CONFIG_DEFINITIONS = {
'PMS_PLEXPASS': (int, 'PMS', 0), 'PMS_PLEXPASS': (int, 'PMS', 0),
'PMS_PLATFORM': (str, 'PMS', ''), 'PMS_PLATFORM': (str, 'PMS', ''),
'PMS_VERSION': (str, 'PMS', ''), 'PMS_VERSION': (str, 'PMS', ''),
'PMS_UPDATE_CHANNEL': (str, 'PMS', 'public'), 'PMS_UPDATE_CHANNEL': (str, 'PMS', 'plex'),
'PMS_UPDATE_DISTRO': (str, 'PMS', ''), 'PMS_UPDATE_DISTRO': (str, 'PMS', ''),
'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''), 'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''),
'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'), 'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
@@ -876,3 +876,9 @@ class Config(object):
self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT self.MUSIC_WATCHED_PERCENT = self.NOTIFY_WATCHED_PERCENT
self.CONFIG_VERSION = 9 self.CONFIG_VERSION = 9
if self.CONFIG_VERSION == 9:
if self.PMS_UPDATE_CHANNEL == 'plexpass':
self.PMS_UPDATE_CHANNEL = 'beta'
self.CONFIG_VERSION = 10

View File

@@ -698,6 +698,10 @@ class Graphs(object):
series_3 = [] series_3 = []
for item in result: for item in result:
if item['resolution'] not in ('4k', 'unknown'):
item['resolution'] = item['resolution'].upper()
if item['resolution'].isdigit():
item['resolution'] += 'p'
categories.append(item['resolution']) categories.append(item['resolution'])
series_1.append(item['dp_count']) series_1.append(item['dp_count'])
series_2.append(item['ds_count']) series_2.append(item['ds_count'])
@@ -729,16 +733,18 @@ class Graphs(object):
try: try:
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT ' \ query = 'SELECT ' \
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \ '(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
'(CASE ' \ '(CASE ' \
'WHEN session_history_media_info.transcode_height <= 360 THEN "sd" ' \ 'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \ 'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \ 'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \ 'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \ 'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \ 'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \ 'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \ 'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) ' \
'ELSE session_history_media_info.stream_video_resolution END) AS resolution, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
'THEN 1 ELSE 0 END) AS dp_count, ' \ 'THEN 1 ELSE 0 END) AS dp_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
@@ -758,16 +764,18 @@ class Graphs(object):
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
query = 'SELECT ' \ query = 'SELECT ' \
'(CASE WHEN session_history_media_info.stream_video_resolution IS NULL THEN ' \
'(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \ '(CASE WHEN session_history_media_info.video_decision = "transcode" THEN ' \
'(CASE ' \ '(CASE ' \
'WHEN session_history_media_info.transcode_height <= 360 THEN "sd" ' \ 'WHEN session_history_media_info.transcode_height <= 360 THEN "SD" ' \
'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \ 'WHEN session_history_media_info.transcode_height <= 480 THEN "480" ' \
'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \ 'WHEN session_history_media_info.transcode_height <= 576 THEN "576" ' \
'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \ 'WHEN session_history_media_info.transcode_height <= 720 THEN "720" ' \
'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \ 'WHEN session_history_media_info.transcode_height <= 1080 THEN "1080" ' \
'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \ 'WHEN session_history_media_info.transcode_height <= 1440 THEN "QHD" ' \
'WHEN session_history_media_info.transcode_height <= 2160 THEN "4K" ' \ 'WHEN session_history_media_info.transcode_height <= 2160 THEN "4k" ' \
'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) AS resolution, ' \ 'ELSE "unknown" END) ELSE session_history_media_info.video_resolution END) ' \
'ELSE session_history_media_info.stream_video_resolution END) AS resolution, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "direct play" ' \
'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \ 'AND session_history.stopped > 0 THEN (session_history.stopped - session_history.started) ' \
' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \ ' - (CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) ELSE 0 END) AS dp_count, ' \
@@ -799,6 +807,10 @@ class Graphs(object):
series_3 = [] series_3 = []
for item in result: for item in result:
if item['resolution'] not in ('4k', 'unknown'):
item['resolution'] = item['resolution'].upper()
if item['resolution'].isdigit():
item['resolution'] += 'p'
categories.append(item['resolution']) categories.append(item['resolution'])
series_1.append(item['dp_count']) series_1.append(item['dp_count'])
series_2.append(item['ds_count']) series_2.append(item['ds_count'])

View File

@@ -544,19 +544,19 @@ class Libraries(object):
filtered_count = len(results) filtered_count = len(results)
# Sort results # Sort results
results = sorted(results, key=lambda k: k['sort_title']) results = sorted(results, key=lambda k: k['sort_title'].lower())
sort_order = json_data['order'] sort_order = json_data['order']
for order in reversed(sort_order): for order in reversed(sort_order):
sort_key = json_data['columns'][int(order['column'])]['data'] sort_key = json_data['columns'][int(order['column'])]['data']
reverse = True if order['dir'] == 'desc' else False reverse = True if order['dir'] == 'desc' else False
if rating_key and sort_key == 'sort_title': if rating_key and sort_key == 'sort_title':
results = sorted(results, key=lambda k: helpers.cast_to_int(k['media_index']), reverse=reverse) results = sorted(results, key=lambda k: helpers.cast_to_int(k['media_index']), reverse=reverse)
elif sort_key == 'file_size' or sort_key == 'bitrate': elif sort_key in ('file_size', 'bitrate', 'added_at', 'last_played', 'play_count'):
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse) results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse)
elif sort_key == 'video_resolution': elif sort_key == 'video_resolution':
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse) results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse)
else: else:
results = sorted(results, key=lambda k: k[sort_key], reverse=reverse) results = sorted(results, key=lambda k: k[sort_key].lower(), reverse=reverse)
total_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results]) total_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results])

View File

@@ -122,8 +122,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
# Add on_concurrent and on_newdevice to queue if action is on_play # Add on_concurrent and on_newdevice to queue if action is on_play
if notify_action == 'on_play': if notify_action == 'on_play':
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_concurrent'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_newdevice'}) plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_newdevice'})
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None): def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
@@ -435,20 +435,6 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','') time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','') duration_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','').replace('a','').replace('A','')
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
if server_times:
updated_at = server_times['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
else:
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
# Get metadata for the item # Get metadata for the item
if session: if session:
rating_key = session['rating_key'] rating_key = session['rating_key']
@@ -660,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,
@@ -777,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'],
@@ -845,40 +839,34 @@ def build_server_notify_params(notify_action=None, **kwargs):
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','') date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','') time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
# Get the server name update_channel = pmsconnect.PmsConnect().get_server_update_channel()
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {})) pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {})) plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
if server_times:
updated_at = server_times['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_int(updated_at)))
else:
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
available_params = { available_params = {
# Global paramaters # Global paramaters
'plexpy_version': common.VERSION_NUMBER, 'tautulli_version': common.VERSION_NUMBER,
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH, 'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
'plexpy_commit': plexpy.CURRENT_VERSION, 'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
'server_name': server_name, 'tautulli_commit': plexpy.CURRENT_VERSION,
'server_uptime': server_uptime, 'server_name': plexpy.CONFIG.PMS_NAME,
'server_version': server_times.get('version', ''), 'server_ip': plexpy.CONFIG.PMS_IP,
'server_port': plexpy.CONFIG.PMS_PORT,
'server_url': plexpy.CONFIG.PMS_URL,
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
'server_version': plexpy.CONFIG.PMS_VERSION,
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
'action': notify_action.lstrip('on_'), 'action': notify_action.lstrip('on_'),
'datestamp': arrow.now().format(date_format), 'datestamp': arrow.now().format(date_format),
'timestamp': arrow.now().format(time_format), 'timestamp': arrow.now().format(time_format),
'unixtime': int(time.time()),
# Plex Media Server update parameters # Plex Media Server update parameters
'update_version': pms_download_info['version'], 'update_version': pms_download_info['version'],
'update_url': pms_download_info['download_url'], 'update_url': pms_download_info['download_url'],
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format) 'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
if pms_download_info['release_date'] else '', if pms_download_info['release_date'] else '',
'update_channel': 'Beta' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public', 'update_channel': 'Beta' if update_channel == 'beta' else 'Public',
'update_platform': pms_download_info['platform'], 'update_platform': pms_download_info['platform'],
'update_distro': pms_download_info['distro'], 'update_distro': pms_download_info['distro'],
'update_distro_build': pms_download_info['build'], 'update_distro_build': pms_download_info['build'],
@@ -887,12 +875,12 @@ def build_server_notify_params(notify_action=None, **kwargs):
'update_changelog_added': pms_download_info['changelog_added'], 'update_changelog_added': pms_download_info['changelog_added'],
'update_changelog_fixed': pms_download_info['changelog_fixed'], 'update_changelog_fixed': pms_download_info['changelog_fixed'],
# Tautulli update parameters # Tautulli update parameters
'plexpy_update_version': plexpy_download_info['tag_name'], 'tautulli_update_version': plexpy_download_info['tag_name'],
'plexpy_update_tar': plexpy_download_info['tarball_url'], 'tautulli_update_tar': plexpy_download_info['tarball_url'],
'plexpy_update_zip': plexpy_download_info['zipball_url'], 'tautulli_update_zip': plexpy_download_info['zipball_url'],
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''), 'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''), 'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''),
'plexpy_update_changelog': plexpy_download_info['body'] 'tautulli_update_changelog': plexpy_download_info['body']
} }
return available_params return available_params
@@ -992,8 +980,8 @@ def strip_tag(data, agent_id=None):
'font': ['color']} 'font': ['color']}
return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True) return bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
elif agent_id == 10: elif agent_id in (10, 14, 20):
# Don't remove tags for email # Don't remove tags for Email, Slack, and Discord
return data return data
elif agent_id == 13: elif agent_id == 13:

View File

@@ -90,7 +90,8 @@ AGENT_IDS = {'growl': 0,
'discord': 20, 'discord': 20,
'androidapp': 21, 'androidapp': 21,
'groupme': 22, 'groupme': 22,
'mqtt': 23 'mqtt': 23,
'zapier': 24
} }
@@ -186,6 +187,10 @@ def available_notification_agents():
{'label': 'XBMC', {'label': 'XBMC',
'name': 'xbmc', 'name': 'xbmc',
'id': AGENT_IDS['xbmc'] 'id': AGENT_IDS['xbmc']
},
{'label': 'Zapier',
'name': 'zapier',
'id': AGENT_IDS['zapier']
} }
] ]
@@ -377,6 +382,8 @@ def get_agent_class(agent_id=None, config=None):
return GROUPME(config=config) return GROUPME(config=config)
elif agent_id == 23: elif agent_id == 23:
return MQTT(config=config) return MQTT(config=config)
elif agent_id == 24:
return ZAPIER(config=config)
else: else:
return Notifier(config=config) return Notifier(config=config)
else: else:
@@ -652,13 +659,28 @@ class PrettyMetadata(object):
provider_name = 'Trakt.tv' provider_name = 'Trakt.tv'
elif provider == 'lastfm': elif provider == 'lastfm':
provider_name = 'Last.fm' provider_name = 'Last.fm'
else:
if self.media_type == 'movie':
provider_name = 'IMDb'
elif self.media_type in ('show', 'season', 'episode'):
provider_name = 'TheTVDB'
elif self.media_type in ('artist', 'album', 'track'):
provider_name = 'Last.fm'
return provider_name return provider_name
def get_provider_link(self, provider=None): def get_provider_link(self, provider=None):
provider_link = ''
if provider == 'plexweb': if provider == 'plexweb':
provider_link = self.get_plex_url() provider_link = self.get_plex_url()
else: elif provider:
provider_link = self.parameters.get(provider + '_url', '') provider_link = self.parameters.get(provider + '_url', '')
else:
if self.media_type == 'movie':
provider_link = self.parameters.get('imdb_url', '')
elif self.media_type in ('show', 'season', 'episode'):
provider_link = self.parameters.get('thetvdb_url', '')
elif self.media_type in ('artist', 'album', 'track'):
provider_link = self.parameters.get('lastfm_url', '')
return provider_link return provider_link
def get_caption(self, provider): def get_caption(self, provider):
@@ -1919,23 +1941,24 @@ class IFTTT(Notifier):
headers=headers, json=data) headers=headers, json=data)
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Ifttt Maker Channel Key', config_option = [{'label': 'IFTTT Webhook Key',
'value': self.config['key'], 'value': self.config['key'],
'name': 'ifttt_key', 'name': 'ifttt_key',
'description': 'Your Ifttt key. You can get a key from' 'description': 'Your IFTTT webhook key. You can get a key from'
' <a href="' + helpers.anon_url('https://ifttt.com/maker') + '" target="_blank">here</a>.', ' <a href="' + helpers.anon_url('https://ifttt.com/maker_webhooks') + '" target="_blank">here</a>.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'Ifttt Event', {'label': 'IFTTT Event',
'value': self.config['event'], 'value': self.config['event'],
'name': 'ifttt_event', 'name': 'ifttt_event',
'description': 'The Ifttt maker event to fire. You can include' 'description': 'The IFTTT maker event to fire. You can include'
' the {action} to be substituted with the action name.' ' <span class="inline-pre">{action}</span>'
' to be substituted with the action name.'
' The notification subject and body will be sent' ' The notification subject and body will be sent'
' as value1 and value2 respectively.', ' as <span class="inline-pre">value1</span>'
' and <span class="inline-pre">value2</span> respectively.',
'input_type': 'text' 'input_type': 'text'
} }
] ]
return config_option return config_option
@@ -2072,7 +2095,7 @@ class JOIN(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'join_movie_provider', 'name': 'join_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', '3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -2080,7 +2103,7 @@ class JOIN(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'join_tv_provider', 'name': 'join_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', '3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -2088,7 +2111,7 @@ class JOIN(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'join_music_provider', 'name': 'join_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.', 'description': 'Select the source for music links in the notificaation. Leave blank for default.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -2714,7 +2737,7 @@ class PUSHOVER(Notifier):
{'label': 'Movie Link Source', {'label': 'Movie Link Source',
'value': self.config['movie_provider'], 'value': self.config['movie_provider'],
'name': 'pushover_movie_provider', 'name': 'pushover_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', '3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers() 'select_options': PrettyMetadata().get_movie_providers()
@@ -2722,7 +2745,7 @@ class PUSHOVER(Notifier):
{'label': 'TV Show Link Source', {'label': 'TV Show Link Source',
'value': self.config['tv_provider'], 'value': self.config['tv_provider'],
'name': 'pushover_tv_provider', 'name': 'pushover_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>' 'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.', '3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers() 'select_options': PrettyMetadata().get_tv_providers()
@@ -2730,7 +2753,7 @@ class PUSHOVER(Notifier):
{'label': 'Music Link Source', {'label': 'Music Link Source',
'value': self.config['music_provider'], 'value': self.config['music_provider'],
'name': 'pushover_music_provider', 'name': 'pushover_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.', 'description': 'Select the source for music links in the notification. Leave blank for default.',
'input_type': 'select', 'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers() 'select_options': PrettyMetadata().get_music_providers()
} }
@@ -3403,6 +3426,104 @@ class XBMC(Notifier):
return config_option return config_option
class ZAPIER(Notifier):
"""
Zapier notifications
"""
NAME = 'Zapier'
_DEFAULT_CONFIG = {'hook': '',
'movie_provider': '',
'tv_provider': '',
'music_provider': ''
}
def _test_hook(self):
_test_data = {'subject': 'Subject',
'body': 'Body',
'action': 'Action',
'poster_url': 'https://i.imgur.com',
'provider_name': 'Provider Name',
'provider_link': 'http://www.imdb.com',
'plex_url': 'https://app.plex.tv/desktop'}
return self.agent_notify(_test_data=_test_data)
def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'subject': subject.encode("utf-8"),
'body': body.encode("utf-8"),
'action': action.encode("utf-8")}
if kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata
pretty_metadata = PrettyMetadata(kwargs['parameters'])
if pretty_metadata.media_type == 'movie':
provider = self.config['movie_provider']
elif pretty_metadata.media_type in ('show', 'season', 'episode'):
provider = self.config['tv_provider']
elif pretty_metadata.media_type in ('artist', 'album', 'track'):
provider = self.config['music_provider']
else:
provider = None
poster_url = pretty_metadata.get_poster_url()
provider_name = pretty_metadata.get_provider_name(provider)
provider_link = pretty_metadata.get_provider_link(provider)
plex_url = pretty_metadata.get_plex_url()
data['poster_url'] = poster_url
data['provider_name'] = provider_name
data['provider_link'] = provider_link
data['plex_url'] = plex_url
if kwargs.get('_test_data'):
data.update(kwargs['_test_data'])
headers = {'Content-type': 'application/json'}
return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self):
config_option = [{'label': 'Zapier Webhook URL',
'value': self.config['hook'],
'name': 'zapier_hook',
'description': 'Your Zapier webhook URL.',
'input_type': 'text'
},
{'label': 'Test Zapier Webhook',
'value': 'Send Test Data',
'name': 'zapier_test_hook',
'description': 'Click this button when prompted on then "Test Webhooks by Zapier" step.',
'input_type': 'button'
},
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'zapier_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
},
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'zapier_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
'3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
},
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'zapier_music_provider',
'description': 'Select the source for music links in the notification. Leave blank for default.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
]
return config_option
def upgrade_config_to_db(): def upgrade_config_to_db():
logger.info(u"Tautulli Notifiers :: Upgrading to new notification system...") logger.info(u"Tautulli Notifiers :: Upgrading to new notification system...")

View File

@@ -376,9 +376,19 @@ class PlexTV(object):
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None, def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
rating_key_filter=None, sync_id_filter=None): rating_key_filter=None, sync_id_filter=None):
if machine_id is None: if not machine_id:
machine_id = plexpy.CONFIG.PMS_IDENTIFIER machine_id = plexpy.CONFIG.PMS_IDENTIFIER
if isinstance(rating_key_filter, list):
rating_key_filter = [str(k) for k in rating_key_filter]
elif rating_key_filter:
rating_key_filter = [str(rating_key_filter)]
if isinstance(user_id_filter, list):
user_id_filter = [str(k) for k in user_id_filter]
elif user_id_filter:
user_id_filter = [str(user_id_filter)]
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml') sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
user_data = users.Users() user_data = users.Users()
@@ -418,7 +428,7 @@ class PlexTV(object):
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt') device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
# Filter by user_id # Filter by user_id
if user_id_filter and str(user_id_filter) != device_user_id: if user_id_filter and device_user_id not in user_id_filter:
continue continue
for synced in a.getElementsByTagName('SyncItems'): for synced in a.getElementsByTagName('SyncItems'):
@@ -432,7 +442,7 @@ class PlexTV(object):
for idx, item in enumerate(clean_uri) if item == 'metadata'), None) for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
# Filter by rating_key # Filter by rating_key
if rating_key_filter and str(rating_key_filter) != rating_key: if rating_key_filter and rating_key not in rating_key_filter:
continue continue
sync_id = helpers.get_xml_attr(item, 'id') sync_id = helpers.get_xml_attr(item, 'id')
@@ -461,12 +471,13 @@ class PlexTV(object):
status_item_downloaded_count, status_item_count) status_item_downloaded_count, status_item_count)
for settings in item.getElementsByTagName('MediaSettings'): for settings in item.getElementsByTagName('MediaSettings'):
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost') settings_video_bitrate = helpers.get_xml_attr(settings, 'maxVideoBitrate')
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality') settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution') settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
settings_audio_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
sync_details = {"device_name": helpers.sanitize(device_name), sync_details = {"device_name": helpers.sanitize(device_name),
"platform": helpers.sanitize(device_platform), "platform": helpers.sanitize(device_platform),
@@ -483,7 +494,8 @@ class PlexTV(object):
"item_complete_count": status_item_complete_count, "item_complete_count": status_item_complete_count,
"item_downloaded_count": status_item_downloaded_count, "item_downloaded_count": status_item_downloaded_count,
"item_downloaded_percent_complete": status_item_download_percent_complete, "item_downloaded_percent_complete": status_item_download_percent_complete,
"music_bitrate": settings_music_bitrate, "video_bitrate": settings_video_bitrate,
"audio_bitrate": settings_audio_bitrate,
"photo_quality": settings_photo_quality, "photo_quality": settings_photo_quality,
"video_quality": settings_video_quality, "video_quality": settings_video_quality,
"total_size": status_total_size, "total_size": status_total_size,
@@ -641,10 +653,14 @@ class PlexTV(object):
def get_plex_downloads(self): def get_plex_downloads(self):
logger.debug(u"Tautulli PlexTV :: Retrieving current server version.") logger.debug(u"Tautulli PlexTV :: Retrieving current server version.")
pmsconnect.PmsConnect().set_server_version()
logger.debug(u"Tautulli PlexTV :: Plex update channel is %s." % plexpy.CONFIG.PMS_UPDATE_CHANNEL) pms_connect = pmsconnect.PmsConnect()
plex_downloads = self.get_plextv_downloads(plexpass=(plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass')) pms_connect.set_server_version()
update_channel = pms_connect.get_server_update_channel()
logger.debug(u"Tautulli PlexTV :: Plex update channel is %s." % update_channel)
plex_downloads = self.get_plextv_downloads(plexpass=(update_channel == 'beta'))
try: try:
available_downloads = json.loads(plex_downloads) available_downloads = json.loads(plex_downloads)

View File

@@ -533,7 +533,7 @@ class PmsConnect(object):
metadata = {} metadata = {}
if cache_key: if cache_key:
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key) in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
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)
@@ -559,27 +559,32 @@ class PmsConnect(object):
for a in xml_head: for a in xml_head:
if a.getAttribute('size'): if a.getAttribute('size'):
if a.getAttribute('size') != '1': if a.getAttribute('size') == '0':
return metadata return metadata
if a.getElementsByTagName('Directory'): if a.getElementsByTagName('Directory'):
metadata_main = a.getElementsByTagName('Directory')[0] metadata_main_list = a.getElementsByTagName('Directory')
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
if metadata_type == 'photo':
metadata_type = 'photo_album'
elif a.getElementsByTagName('Video'): elif a.getElementsByTagName('Video'):
metadata_main = a.getElementsByTagName('Video')[0] metadata_main_list = a.getElementsByTagName('Video')
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
elif a.getElementsByTagName('Track'): elif a.getElementsByTagName('Track'):
metadata_main = a.getElementsByTagName('Track')[0] metadata_main_list = a.getElementsByTagName('Track')
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
elif a.getElementsByTagName('Photo'): elif a.getElementsByTagName('Photo'):
metadata_main = a.getElementsByTagName('Photo')[0] metadata_main_list = a.getElementsByTagName('Photo')
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
else: else:
logger.debug(u"Tautulli Pmsconnect :: Metadata failed") logger.debug(u"Tautulli Pmsconnect :: Metadata failed")
return {} return {}
if sync_id and len(metadata_main_list) > 1:
for metadata_main in metadata_main_list:
if helpers.get_xml_attr(metadata_main, 'ratingKey') == rating_key:
break
else:
metadata_main = metadata_main_list[0]
metadata_type = helpers.get_xml_attr(metadata_main, 'type')
if metadata_main.nodeName == 'Directory' and metadata_type == 'photo':
metadata_type = 'photo_album'
section_id = helpers.get_xml_attr(a, 'librarySectionID') section_id = helpers.get_xml_attr(a, 'librarySectionID')
library_name = helpers.get_xml_attr(a, 'librarySectionTitle') library_name = helpers.get_xml_attr(a, 'librarySectionTitle')
@@ -588,6 +593,7 @@ class PmsConnect(object):
actors = [] actors = []
genres = [] genres = []
labels = [] labels = []
collections = []
if metadata_main.getElementsByTagName('Director'): if metadata_main.getElementsByTagName('Director'):
for director in metadata_main.getElementsByTagName('Director'): for director in metadata_main.getElementsByTagName('Director'):
@@ -609,6 +615,10 @@ class PmsConnect(object):
for label in metadata_main.getElementsByTagName('Label'): for label in metadata_main.getElementsByTagName('Label'):
labels.append(helpers.get_xml_attr(label, 'tag')) labels.append(helpers.get_xml_attr(label, 'tag'))
if metadata_main.getElementsByTagName('Collection'):
for collection in metadata_main.getElementsByTagName('Collection'):
collections.append(helpers.get_xml_attr(collection, 'tag'))
if metadata_type == 'movie': if metadata_type == 'movie':
metadata = {'media_type': metadata_type, metadata = {'media_type': metadata_type,
'section_id': section_id, 'section_id': section_id,
@@ -646,6 +656,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title')
} }
@@ -686,6 +697,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title')
} }
@@ -728,6 +740,7 @@ class PmsConnect(object):
'actors': show_details['actors'], 'actors': show_details['actors'],
'genres': show_details['genres'], 'genres': show_details['genres'],
'labels': show_details['labels'], 'labels': show_details['labels'],
'collections': show_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')) helpers.get_xml_attr(metadata_main, 'title'))
} }
@@ -771,6 +784,7 @@ class PmsConnect(object):
'actors': show_details['actors'], 'actors': show_details['actors'],
'genres': show_details['genres'], 'genres': show_details['genres'],
'labels': show_details['labels'], 'labels': show_details['labels'],
'collections': show_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title')) helpers.get_xml_attr(metadata_main, 'title'))
} }
@@ -812,6 +826,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title')
} }
@@ -854,6 +869,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')) helpers.get_xml_attr(metadata_main, 'title'))
} }
@@ -897,6 +913,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': album_details['genres'], 'genres': album_details['genres'],
'labels': album_details['labels'], 'labels': album_details['labels'],
'collections': album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title')) helpers.get_xml_attr(metadata_main, 'title'))
} }
@@ -938,6 +955,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title')
} }
@@ -980,6 +998,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': photo_album_details['genres'], 'genres': photo_album_details['genres'],
'labels': photo_album_details['labels'], 'labels': photo_album_details['labels'],
'collections': photo_album_details['collections'],
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), 'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')) helpers.get_xml_attr(metadata_main, 'title'))
} }
@@ -1025,6 +1044,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title')
} }
@@ -1065,6 +1085,7 @@ class PmsConnect(object):
'actors': actors, 'actors': actors,
'genres': genres, 'genres': genres,
'labels': labels, 'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(metadata_main, 'title') 'full_title': helpers.get_xml_attr(metadata_main, 'title')
} }
@@ -1158,7 +1179,7 @@ class PmsConnect(object):
if cache_key: if cache_key:
metadata['_cache_time'] = int(time.time()) metadata['_cache_time'] = int(time.time())
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key) out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
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)
@@ -1370,7 +1391,7 @@ class PmsConnect(object):
else: else:
session_details = {'session_id': '', session_details = {'session_id': '',
'bandwidth': '', 'bandwidth': '',
'location': 'Unknown' 'location': 'wan' if player_details['local'] == '0' else 'lan'
} }
# Get the transcode details # Get the transcode details
@@ -1443,16 +1464,24 @@ class PmsConnect(object):
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \ if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play': and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
plex_tv = plextv.PlexTV() plex_tv = plextv.PlexTV()
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'], synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
rating_key_filter=rating_key) rating_key_filter=[rating_key, parent_rating_key, grandparent_rating_key])
if synced_items: if synced_items:
sync_id = synced_items[0]['sync_id'] synced_item_details = synced_items[0]
sync_id = synced_item_details['sync_id']
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml') synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer') synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
if synced_xml_head[0].getElementsByTagName('Track'): if synced_xml_head[0].getElementsByTagName('Track'):
synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0] synced_xml_items = synced_xml_head[0].getElementsByTagName('Track')
elif synced_xml_head[0].getElementsByTagName('Video'): elif synced_xml_head[0].getElementsByTagName('Video'):
synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0] synced_xml_items = synced_xml_head[0].getElementsByTagName('Video')
for synced_session_data in synced_xml_items:
if helpers.get_xml_attr(synced_session_data, 'ratingKey') == rating_key:
break
# Figure out which version is being played # Figure out which version is being played
if sync_id: if sync_id:
@@ -1586,6 +1615,7 @@ class PmsConnect(object):
channel_stream = 1 channel_stream = 1
clip_media = session.getElementsByTagName('Media')[0] clip_media = session.getElementsByTagName('Media')[0]
clip_part = clip_media.getElementsByTagName('Part')[0]
audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels') audio_channels = helpers.get_xml_attr(clip_media, 'audioChannels')
metadata_details = {'media_type': media_type, metadata_details = {'media_type': media_type,
'section_id': helpers.get_xml_attr(session, 'librarySectionID'), 'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
@@ -1624,7 +1654,8 @@ class PmsConnect(object):
'genres': [], 'genres': [],
'labels': [], 'labels': [],
'full_title': helpers.get_xml_attr(session, 'title'), 'full_title': helpers.get_xml_attr(session, 'title'),
'container': helpers.get_xml_attr(clip_media, 'container'), 'container': helpers.get_xml_attr(clip_media, 'container') \
or helpers.get_xml_attr(clip_part, 'container'),
'height': helpers.get_xml_attr(clip_media, 'height'), 'height': helpers.get_xml_attr(clip_media, 'height'),
'width': helpers.get_xml_attr(clip_media, 'width'), 'width': helpers.get_xml_attr(clip_media, 'width'),
'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'), 'video_codec': helpers.get_xml_attr(clip_media, 'videoCodec'),
@@ -1633,7 +1664,8 @@ class PmsConnect(object):
'audio_channels': audio_channels, 'audio_channels': audio_channels,
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels), 'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'), 'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
'channel_title': helpers.get_xml_attr(session, 'sourceTitle') 'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
'live': int(helpers.get_xml_attr(session, 'live') == '1')
} }
else: else:
channel_stream = 0 channel_stream = 0
@@ -1642,7 +1674,7 @@ class PmsConnect(object):
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id') part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
if sync_id: if sync_id:
metadata_details = self.get_metadata_details(sync_id=sync_id, cache_key=session_key) metadata_details = self.get_metadata_details(rating_key=rating_key, sync_id=sync_id, cache_key=session_key)
else: else:
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key) metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
@@ -1699,51 +1731,72 @@ class PmsConnect(object):
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id), source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details)) next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
# Overrides for live sessions
if metadata_details.get('live') and transcode_decision == 'transcode':
stream_details['stream_container_decision'] = 'transcode'
stream_details['stream_container'] = transcode_details['transcode_container']
video_details['stream_video_decision'] = transcode_details['video_decision']
stream_details['stream_video_codec'] = transcode_details['transcode_video_codec']
stream_details['stream_video_resolution'] = metadata_details['video_resolution']
audio_details['stream_audio_decision'] = transcode_details['audio_decision']
stream_details['stream_audio_codec'] = transcode_details['transcode_audio_codec']
stream_details['stream_audio_channels'] = transcode_details['transcode_audio_channels']
stream_details['stream_audio_channel_layout'] = common.AUDIO_CHANNELS.get(
transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels'])
# Get the quality profile # Get the quality profile
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details: if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate']) if sync_id:
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
try:
quailtiy_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate]
except ValueError:
quality_profile = 'Original' quality_profile = 'Original'
if sync_id: synced_item_bitrate = helpers.cast_to_int(synced_item_details['video_bitrate'])
try: try:
synced_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if source_bitrate <= b) synced_bitrate = max(b for b in common.VIDEO_QUALITY_PROFILES if b <= synced_item_bitrate)
synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate] synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate]
except ValueError: except ValueError:
synced_version_profile = 'Original' synced_version_profile = 'Original'
else: else:
synced_version_profile = '' synced_version_profile = ''
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
try:
quailtiy_bitrate = min(
b for b in common.VIDEO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate]
except ValueError:
quality_profile = 'Original'
if stream_details['optimized_version']: if stream_details['optimized_version']:
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1), optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'], source_media_details['video_resolution'])) plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'],
source_media_details['video_resolution']))
else: else:
optimized_version_profile = '' optimized_version_profile = ''
elif media_type == 'track' and 'stream_bitrate' in stream_details: elif media_type == 'track' and 'stream_bitrate' in stream_details:
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate']) if sync_id:
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
try:
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
except ValueError:
quality_profile = 'Original' quality_profile = 'Original'
if sync_id: synced_item_bitrate = helpers.cast_to_int(synced_item_details['audio_bitrate'])
try: try:
synced_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if source_bitrate <= b) synced_bitrate = max(b for b in common.AUDIO_QUALITY_PROFILES if b <= synced_item_bitrate)
synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate] synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate]
except ValueError: except ValueError:
synced_version_profile = 'Original' synced_version_profile = 'Original'
else: else:
synced_version_profile = '' synced_version_profile = ''
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
try:
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
except ValueError:
quality_profile = 'Original'
optimized_version_profile = '' optimized_version_profile = ''
elif media_type == 'photo': elif media_type == 'photo':
@@ -2122,8 +2175,12 @@ class PmsConnect(object):
item_main += a.getElementsByTagName('Photo') item_main += a.getElementsByTagName('Photo')
for item in item_main: for item in item_main:
media_type = helpers.get_xml_attr(item, 'type')
if item.nodeName == 'Directory' and media_type == 'photo':
media_type = 'photo_album'
item_info = {'section_id': helpers.get_xml_attr(a, 'librarySectionID'), item_info = {'section_id': helpers.get_xml_attr(a, 'librarySectionID'),
'media_type': helpers.get_xml_attr(item, 'type'), 'media_type': media_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'), 'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'), 'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(item, 'grandparentRatingKey'),
@@ -2533,4 +2590,15 @@ class PmsConnect(object):
version = identity.get('version', plexpy.CONFIG.PMS_VERSION) version = identity.get('version', plexpy.CONFIG.PMS_VERSION)
plexpy.CONFIG.__setattr__('PMS_VERSION', version) plexpy.CONFIG.__setattr__('PMS_VERSION', version)
plexpy.CONFIG.write() plexpy.CONFIG.write()
def get_server_update_channel(self):
if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plex':
update_channel_value = self.get_server_pref('ButlerUpdateChannel')
if update_channel_value == '8':
return 'beta'
else:
return 'public'
return plexpy.CONFIG.PMS_UPDATE_CHANNEL

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.14-beta" PLEXPY_RELEASE_VERSION = "v2.0.17-beta"

View File

@@ -674,7 +674,7 @@ class WebInterface(object):
if not kwargs.get('json_data'): if not kwargs.get('json_data'):
# Alias 'title' to 'sort_title' # Alias 'title' to 'sort_title'
if kwargs.get('order_column') == 'title': if kwargs.get('order_column') == 'title':
kwargs['order_column'] == 'sort_title' kwargs['order_column'] = 'sort_title'
# TODO: Find some one way to automatically get the columns # TODO: Find some one way to automatically get the columns
dt_columns = [("added_at", True, False), dt_columns = [("added_at", True, False),
@@ -2201,9 +2201,8 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
def get_sync(self, machine_id=None, user_id=None, **kwargs): def get_sync(self, machine_id=None, user_id=None, **kwargs):
if user_id == 'null':
if not machine_id: user_id = None
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
plex_tv = plextv.PlexTV() plex_tv = plextv.PlexTV()
result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id) result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id)
@@ -2798,12 +2797,16 @@ class WebInterface(object):
def get_server_update_params(self, **kwargs): def get_server_update_params(self, **kwargs):
plex_tv = plextv.PlexTV() plex_tv = plextv.PlexTV()
plexpass = plex_tv.get_plexpass_status() plexpass = plex_tv.get_plexpass_status()
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
return {'plexpass': plexpass, return {'plexpass': plexpass,
'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get( 'pms_platform': common.PMS_PLATFORM_NAME_OVERRIDES.get(
plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM), plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM),
'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL, 'pms_update_channel': plexpy.CONFIG.PMS_UPDATE_CHANNEL,
'pms_update_distro': plexpy.CONFIG.PMS_UPDATE_DISTRO, 'pms_update_distro': plexpy.CONFIG.PMS_UPDATE_DISTRO,
'pms_update_distro_build': plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD} 'pms_update_distro_build': plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD,
'plex_update_channel': 'plexpass' if update_channel == 'beta' else 'public'}
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@@ -3197,6 +3200,16 @@ class WebInterface(object):
logger.warn(msg) logger.warn(msg)
return msg return msg
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def zapier_test_hook(self, zapier_hook='', **kwargs):
success = notifiers.ZAPIER(config={'hook': zapier_hook})._test_hook()
if success:
return {'result': 'success', 'msg': 'Test Zapier webhook sent.'}
else:
return {'result': 'error', 'msg': 'Failed to send test Zapier webhook.'}
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
def set_notification_config(self, **kwargs): def set_notification_config(self, **kwargs):