Compare commits
40 Commits
v2.0.6-bet
...
v2.0.12-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
644fea6665 | ||
![]() |
a1349ff8a6 | ||
![]() |
71c20002b8 | ||
![]() |
157af84226 | ||
![]() |
9b4536f132 | ||
![]() |
29ab470e42 | ||
![]() |
c67fa480a7 | ||
![]() |
0a1a691c73 | ||
![]() |
48588f23bf | ||
![]() |
cf14fbc3f0 | ||
![]() |
e471d5207d | ||
![]() |
5722a52082 | ||
![]() |
08c32e875e | ||
![]() |
7d3ee3afb3 | ||
![]() |
def8600f5c | ||
![]() |
74a68f3c7d | ||
![]() |
64c9247dd1 | ||
![]() |
1bfcd34247 | ||
![]() |
19864e97e6 | ||
![]() |
ec5c5e1420 | ||
![]() |
803f4e14ca | ||
![]() |
6cc254b80a | ||
![]() |
59593ab1aa | ||
![]() |
65a0a0eb7d | ||
![]() |
f4206b401f | ||
![]() |
99f8d24b3e | ||
![]() |
26b06e453d | ||
![]() |
54ab646048 | ||
![]() |
12c9aa3d6a | ||
![]() |
1ae8544f2d | ||
![]() |
eae9e66c75 | ||
![]() |
ad041a1691 | ||
![]() |
1aee3b6c8f | ||
![]() |
04d4ffb63d | ||
![]() |
80b318b45c | ||
![]() |
19969a8b1f | ||
![]() |
b84888356f | ||
![]() |
c9436195f3 | ||
![]() |
98cfb50571 | ||
![]() |
b67884ea7f |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.12-beta (2018-01-07)
|
||||
|
||||
* Notifications:
|
||||
* Fix: Incorrect Plex URL parameter value.
|
||||
* Change: Custom condition logic is now optional. An implicit "and" is applied between all conditions if the logic is blank.
|
||||
* UI:
|
||||
* New: Added separate required LAN/WAN bandwidth in the activity header.
|
||||
* API:
|
||||
* Fix: Notify API command not sending notifications.
|
||||
|
||||
|
||||
## v2.0.11-beta (2018-01-05)
|
||||
|
||||
* Notifications:
|
||||
* Fix: Some notification parameters showing up blank.
|
||||
* UI:
|
||||
* Fix: Stream data showing up as "None" for pre-v2 history.
|
||||
* Other:
|
||||
* Fix: Ability to login using the hashed password.
|
||||
|
||||
|
||||
## v2.0.10-beta (2018-01-04)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: HW transcoding indicator on activity cards incorrect after refreshing.
|
||||
* Notifications:
|
||||
* Remove: Notification toggles from library and user settings. Use custom conditions to filter out notifications instead.
|
||||
* UI:
|
||||
* Fix: Incorrect examples for some date format options. Also added a few missing date format options. (Thanks @Tommatheussen)
|
||||
|
||||
|
||||
## v2.0.9-beta (2018-01-03)
|
||||
|
||||
* Notifications:
|
||||
* Fix: Notifications failing due to incorrect season/episode number types.
|
||||
|
||||
|
||||
## v2.0.8-beta (2018-01-03)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Incorrect HW transcoding indicator on activity cards.
|
||||
* Fix: Long product/player names hidden behind platform icon on activity cards.
|
||||
* Notifications:
|
||||
* Fix: Notifications failing due to some missing notification parameters.
|
||||
|
||||
|
||||
## v2.0.7-beta (2018-01-01)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Incorrect LAN/WAN location on activity cards.
|
||||
* Fix: Paused time not recording correctly.
|
||||
* Other:
|
||||
* Fix: Failed to retrieve synced items when there are special characters in the title.
|
||||
|
||||
|
||||
## v2.0.6-beta (2017-12-31)
|
||||
|
||||
* Monitoring:
|
||||
@@ -17,8 +72,8 @@
|
||||
* Fix: Error sending Join notifications.
|
||||
* UI:
|
||||
* New: Added total required bandwidth in the activity header.
|
||||
* Fix: Failing to retrieve releases from GitHub.
|
||||
* Other:
|
||||
* Fix: Failing to retrieve releases from GitHub.
|
||||
* Fix: CherryPy SSL connection warning. (Thanks @felixbuenemann)
|
||||
* Fix: Sanitize script output in logs.
|
||||
* Change: Login sessions persists across server restarts.
|
||||
|
@@ -64,7 +64,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand svg" href="home">
|
||||
<a class="navbar-brand" href="home" title="Tautulli">
|
||||
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -13,18 +13,6 @@ a:focus {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
a.svg {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
a.svg:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
select, .react-selectize.bootstrap3.root-node .react-selectize-control {
|
||||
margin: 5px 0 5px 0;
|
||||
border: 2px solid #444;
|
||||
@@ -83,11 +71,13 @@ select.form-control {
|
||||
border-radius: 3px;
|
||||
transition: background-color .3s;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control {
|
||||
.react-selectize.root-node .react-selectize-control,
|
||||
.selectize-control.form-control .selectize-input {
|
||||
color: #fff !important;
|
||||
border: 0px solid #444 !important;
|
||||
background: #555 !important;
|
||||
padding: 1px 2px;
|
||||
transition: background-color .3s;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
||||
color: #fff !important;
|
||||
@@ -95,6 +85,13 @@ select.form-control {
|
||||
.react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
|
||||
fill: #fff !important;
|
||||
}
|
||||
.react-selectize.root-node .simple-value,
|
||||
.selectize-control.multi .selectize-input > div {
|
||||
background: #444444 !important;
|
||||
color: #ffffff !important;
|
||||
padding-bottom: 2px !important;
|
||||
transition: background-color .3s;
|
||||
}
|
||||
.react-selectize.root-node .simple-value span {
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
@@ -102,13 +99,25 @@ select.form-control {
|
||||
padding-top: 3px !important;
|
||||
padding-bottom: 3px !important;
|
||||
}
|
||||
select.form-control:focus {
|
||||
select.form-control:focus,
|
||||
.react-selectize.root-node.open .react-selectize-control,
|
||||
.selectize-control.form-control .selectize-input.focus {
|
||||
outline: 0;
|
||||
outline: thin dotted \9;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
color: #555 !important;
|
||||
background-color: #fff !important;
|
||||
transition: background-color .3s;
|
||||
}
|
||||
.react-selectize.root-node.open .simple-value,
|
||||
.selectize-control.multi .selectize-input.focus > div,
|
||||
.selectize-control.multi .selectize-input > div.active{
|
||||
background: #efefef !important;
|
||||
color: #333333 !important;
|
||||
transition: background-color .3s;
|
||||
}
|
||||
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
||||
fill: #999 !important;
|
||||
}
|
||||
select.form-control option {
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
@@ -118,6 +127,9 @@ img {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
object {
|
||||
pointer-events: none;
|
||||
}
|
||||
.navbar {
|
||||
background: #000;
|
||||
-webkit-box-shadow: 0 0 0 3px rgba(0,0,0,.2);
|
||||
@@ -853,6 +865,18 @@ a .users-poster-face:hover {
|
||||
-webkit-flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.dashboard-activity-info-item .sub-value.platform-right {
|
||||
margin-right: 55px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dashboard-activity-info-item .sub-value.time-right {
|
||||
margin-right: 60px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dashboard-activity-info-item .sub-value .ip-container {
|
||||
display: inline-flex;
|
||||
}
|
||||
@@ -1270,7 +1294,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
|
||||
.dashboard-stats-info {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
padding: 3px 0 5px 15px;
|
||||
padding: 3px 0 0 15px;
|
||||
position: relative;
|
||||
}
|
||||
.dashboard-stats-info-list {
|
||||
@@ -3719,7 +3743,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
.no-image {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
#info-modal .stream-info-current {
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#info-modal .stream-info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@@ -64,6 +64,7 @@ DOCUMENTATION :: END
|
||||
from collections import defaultdict
|
||||
from urllib import quote
|
||||
from plexpy import helpers
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||
import plexpy
|
||||
%>
|
||||
<% data = defaultdict(lambda: 'Unknown', **session) %>
|
||||
@@ -134,15 +135,15 @@ DOCUMENTATION :: END
|
||||
<ul class="list-unstyled dashboard-activity-info-list">
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Product</div>
|
||||
<div class="sub-value">${data['product']}</div>
|
||||
<div class="sub-value platform-right">${data['product']}</div>
|
||||
</li>
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Player</div>
|
||||
<div class="sub-value">${data['player']}</div>
|
||||
<div class="sub-value platform-right">${data['player']}</div>
|
||||
</li>
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Quality</div>
|
||||
<div class="sub-value" id="stream_quality-${sk}">
|
||||
<div class="sub-value platform-right" id="stream_quality-${sk}">
|
||||
% if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown':
|
||||
<%
|
||||
br = helpers.cast_to_int(data['stream_bitrate']) or ''
|
||||
@@ -214,17 +215,14 @@ DOCUMENTATION :: END
|
||||
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
||||
% if data.get('stream_video_decision') == 'transcode':
|
||||
<%
|
||||
hw_d = hw_e = ''
|
||||
if data['transcode_hw_requested'] == 1 and data['transcode_hw_full_pipeline'] == 0:
|
||||
hw_d = ' (HW)'
|
||||
elif data['transcode_hw_requested'] == 1 and data['transcode_hw_full_pipeline'] == 1:
|
||||
hw_d = hw_e = ' (HW)'
|
||||
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
||||
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
||||
%>
|
||||
Transcode (${data['video_codec'].upper()}${hw_d} ${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} → ${data['stream_video_codec'].upper()}${hw_e} ${plexpy.common.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'])} → ${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':
|
||||
Direct Stream (${data['stream_video_codec'].upper()} ${plexpy.common.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:
|
||||
Direct Play (${data['video_codec'].upper()} ${plexpy.common.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'])})
|
||||
% endif
|
||||
% elif data['media_type'] == 'photo':
|
||||
Direct Play (${data['width']}x${data['height']})
|
||||
@@ -237,11 +235,11 @@ DOCUMENTATION :: END
|
||||
<div class="sub-heading">Audio</div>
|
||||
<div class="sub-value" id="audio_decision-${sk}">
|
||||
% if data.get('stream_audio_decision') == 'transcode':
|
||||
Transcode (${plexpy.common.AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} → ${plexpy.common.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()} → ${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':
|
||||
Direct Stream (${plexpy.common.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:
|
||||
Direct Play (${plexpy.common.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()})
|
||||
% endif
|
||||
</div>
|
||||
</li>
|
||||
@@ -270,9 +268,9 @@ DOCUMENTATION :: END
|
||||
<ul class="list-unstyled dashboard-activity-info-list">
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Location</div>
|
||||
<div class="sub-value">
|
||||
<div class="sub-value time-right">
|
||||
% if data['ip_address'] != 'N/A':
|
||||
${'LAN' if data['local'] == 1 else 'WAN'}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
${data['location'].upper()}: <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']}">
|
||||
<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>
|
||||
@@ -290,7 +288,7 @@ DOCUMENTATION :: END
|
||||
</li>
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Bandwidth</div>
|
||||
<div class="sub-value">
|
||||
<div class="sub-value time-right">
|
||||
% if data['media_type'] != 'photo' and helpers.cast_to_int(data['bandwidth']):
|
||||
<%
|
||||
bw = helpers.cast_to_int(data['bandwidth'])
|
||||
|
@@ -47,24 +47,12 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<p class="help-block">Change the library's picture in Tautulli. To reset to default, leave this field empty and save.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${helpers.checked(data['do_notify'])}> Enable notifications
|
||||
</label>
|
||||
<p class="help-block">Uncheck this if you do not want to receive notifications for this library's activity.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history
|
||||
</label>
|
||||
<p class="help-block">Uncheck this if you do not want to keep any history on this library's activity.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="do_notify_created" name="do_notify_created" value="1" ${helpers.checked(data['do_notify_created'])}> Enable recently added notifications
|
||||
</label>
|
||||
<p class="help-block">Uncheck this if you do not want to receive recently added notifications for this library.</p>
|
||||
</div>
|
||||
% if data['section_id']:
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger" id="delete-all-history">Purge</button>
|
||||
@@ -85,15 +73,7 @@ DOCUMENTATION :: END
|
||||
// Save library options
|
||||
$("#save_library").on('click', function () {
|
||||
var custom_thumb = $("#custom_thumb_url").val();
|
||||
var do_notify = 0;
|
||||
var do_notify_created = 0;
|
||||
var keep_history = 0;
|
||||
if ($("#do_notify").is(":checked")) {
|
||||
do_notify = 1;
|
||||
}
|
||||
if ($("#do_notify_created").is(":checked")) {
|
||||
do_notify_created = 1;
|
||||
}
|
||||
if ($("#keep_history").is(":checked")) {
|
||||
keep_history = 1;
|
||||
}
|
||||
@@ -103,8 +83,6 @@ DOCUMENTATION :: END
|
||||
data: {
|
||||
section_id: '${data["section_id"]}',
|
||||
custom_thumb: custom_thumb,
|
||||
do_notify: do_notify,
|
||||
do_notify_created: do_notify_created,
|
||||
keep_history: keep_history
|
||||
},
|
||||
cache: false,
|
||||
|
@@ -56,12 +56,6 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<p class="help-block">Change the users profile picture in Tautulli. To reset to default, leave this field empty and save.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${helpers.checked(data['do_notify'])}> Enable notifications
|
||||
</label>
|
||||
<p class="help-block">Uncheck this if you do not want to receive notifications for this user's activity.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history
|
||||
@@ -95,12 +89,8 @@ DOCUMENTATION :: END
|
||||
$("#save_user").on('click', function () {
|
||||
var friendly_name = $("input#friendly_name").val();
|
||||
var custom_thumb = $("#custom_avatar_url").val();
|
||||
var do_notify = 0;
|
||||
var keep_history = 0;
|
||||
var allow_guest = 0;
|
||||
if ($("#do_notify").is(":checked")) {
|
||||
do_notify = 1;
|
||||
}
|
||||
if ($("#keep_history").is(":checked")) {
|
||||
keep_history = 1;
|
||||
}
|
||||
@@ -114,7 +104,6 @@ DOCUMENTATION :: END
|
||||
user_id: '${data["user_id"]}',
|
||||
friendly_name: friendly_name,
|
||||
custom_thumb: custom_thumb,
|
||||
do_notify: do_notify,
|
||||
keep_history: keep_history,
|
||||
allow_guest: allow_guest
|
||||
},
|
||||
|
@@ -292,7 +292,9 @@
|
||||
var sc_dp = current_activity.stream_count_direct_play,
|
||||
sc_ds = current_activity.stream_count_direct_stream,
|
||||
sc_tc = current_activity.stream_count_transcode,
|
||||
total_bw = current_activity.total_bandwidth;
|
||||
total_bw = current_activity.total_bandwidth,
|
||||
lan_bw = current_activity.lan_bandwidth,
|
||||
wan_bw = current_activity.wan_bandwidth;
|
||||
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
||||
if (sc_dp) {
|
||||
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
||||
@@ -306,7 +308,14 @@
|
||||
streams_header = streams_header.replace(/, $/, '') + ')';
|
||||
$('#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')) + ' (';
|
||||
if (lan_bw) {
|
||||
bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
||||
}
|
||||
if (wan_bw) {
|
||||
bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
||||
}
|
||||
bandwidth_header = bandwidth_header.replace(/, $/, '') + ')';
|
||||
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
||||
|
||||
$('#currentActivityHeader').show();
|
||||
@@ -325,25 +334,26 @@
|
||||
}
|
||||
|
||||
// Update play state icon
|
||||
var state_icon = '';
|
||||
switch (s.state) {
|
||||
case 'playing':
|
||||
var state_icon = '<i class="fa fa-fw fa-play"></i> ';
|
||||
state_icon = '<i class="fa fa-fw fa-play"></i> ';
|
||||
break;
|
||||
case 'paused':
|
||||
var state_icon = '<i class="fa fa-fw fa-pause"></i> ';
|
||||
state_icon = '<i class="fa fa-fw fa-pause"></i> ';
|
||||
break;
|
||||
case 'buffering':
|
||||
var state_icon = '<i class="fa fa-fw fa-spinner"></i> ';
|
||||
state_icon = '<i class="fa fa-fw fa-spinner"></i> ';
|
||||
break;
|
||||
default:
|
||||
var state_icon = '<i class="fa fa-fw fa-question-circle"></i> ';
|
||||
state_icon = '<i class="fa fa-fw fa-question-circle"></i> ';
|
||||
}
|
||||
$('#play-state-' + key).html(state_icon).attr('title', capitalizeFirstLetter(s.state));
|
||||
|
||||
// Switching tracks can be under the same session key, so need to update the info.
|
||||
if (s.media_type === 'track') {
|
||||
// Update if artist changed
|
||||
if (s.grandparent_rating_key != instance.data('grandparent_rating_key')) {
|
||||
if (s.grandparent_rating_key !== instance.data('grandparent_rating_key')) {
|
||||
$('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&fallback=art&refresh=true)');
|
||||
$('#metadata-grandparent_title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
|
||||
@@ -351,7 +361,7 @@
|
||||
.text(s.grandparent_title);
|
||||
}
|
||||
// Update cover if album changed
|
||||
if (s.parent_rating_key != instance.data('parent_rating_key')) {
|
||||
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
|
||||
$('#poster-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
|
||||
$('#poster-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
|
||||
$('#poster-url-' + key)
|
||||
@@ -363,7 +373,7 @@
|
||||
.text(s.parent_title);
|
||||
}
|
||||
// Update cover if track changed
|
||||
if (s.parent_rating_key != instance.data('parent_rating_key')) {
|
||||
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
|
||||
$('#metadata-title-' + key)
|
||||
.attr('href', 'info?rating_key=' + s.rating_key)
|
||||
.attr('title', s.title)
|
||||
@@ -374,7 +384,7 @@
|
||||
// Update the transcode state
|
||||
var transcode_decision = '';
|
||||
if (s.transcode_decision === 'transcode') {
|
||||
var throttled = (s.transcode_throttled == 1) ? ' (Throttled)' : ' (Speed: ' + s.transcode_speed + ')';
|
||||
var throttled = (s.transcode_throttled === 1) ? ' (Throttled)' : ' (Speed: ' + s.transcode_speed + ')';
|
||||
transcode_decision = 'Transcode' + throttled;
|
||||
} else if (s.transcode_decision === 'copy') {
|
||||
transcode_decision = 'Direct Stream';
|
||||
@@ -392,36 +402,32 @@
|
||||
$('#transcode_container-' + key).html(transcode_container);
|
||||
|
||||
var video_decision = '';
|
||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.video_decision != '') {
|
||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.video_decision !== '') {
|
||||
var v_res= '';
|
||||
switch (s.video_resolution.toLowerCase()) {
|
||||
case 'sd':
|
||||
var v_res = 'SD';
|
||||
v_res = 'SD';
|
||||
break;
|
||||
case '4k':
|
||||
var v_res = '4k';
|
||||
v_res = '4k';
|
||||
break;
|
||||
default:
|
||||
var v_res = s.video_resolution + 'p'
|
||||
v_res = s.video_resolution + 'p'
|
||||
}
|
||||
var sv_res = '';
|
||||
switch (s.stream_video_resolution.toLowerCase()) {
|
||||
case 'sd':
|
||||
var sv_res = 'SD';
|
||||
sv_res = 'SD';
|
||||
break;
|
||||
case '4k':
|
||||
var sv_res = '4k';
|
||||
sv_res = '4k';
|
||||
break;
|
||||
default:
|
||||
var sv_res = s.stream_video_resolution + 'p'
|
||||
sv_res = s.stream_video_resolution + 'p'
|
||||
}
|
||||
if (s.stream_video_decision === 'transcode') {
|
||||
var hw_d = '';
|
||||
var hw_e = '';
|
||||
if (s.transcode_hw_requested === 1 && s.transcode_hw_full_pipeline === 0) {
|
||||
hw_d = ' (HW)';
|
||||
} else if (s.transcode_hw_requested === 1 && s.transcode_hw_full_pipeline === 1) {
|
||||
hw_d = ' (HW)';
|
||||
hw_e = ' (HW)';
|
||||
}
|
||||
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
||||
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
||||
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' → ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
||||
} else if (s.stream_video_decision === 'copy') {
|
||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
||||
@@ -434,7 +440,7 @@
|
||||
$('#video_decision-' + key).html(video_decision);
|
||||
|
||||
var audio_decision = '';
|
||||
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.audio_codec) {
|
||||
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.audio_decision) {
|
||||
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
||||
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
||||
if (s.stream_audio_decision === 'transcode') {
|
||||
@@ -456,13 +462,13 @@
|
||||
} else if (s.stream_subtitle_decision === 'burn') {
|
||||
subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')';
|
||||
} else {
|
||||
subtitle_decision = 'Direct Play (' + ((s.synced_version == '1') ? s.stream_subtitle_codec.toUpperCase() : s.subtitle_codec.toUpperCase()) + ')';
|
||||
subtitle_decision = 'Direct Play (' + ((s.synced_version === '1') ? s.stream_subtitle_codec.toUpperCase() : s.subtitle_codec.toUpperCase()) + ')';
|
||||
}
|
||||
}
|
||||
$('#subtitle_decision-' + key).html(subtitle_decision);
|
||||
|
||||
// Update the stream quality profile and bandwidth
|
||||
if (s.media_type != 'photo' && s.quality_profile != 'Unknown') {
|
||||
if (s.media_type !== 'photo' && s.quality_profile !== 'Unknown') {
|
||||
var br = parseInt(s.stream_bitrate) || '';
|
||||
if (br) {
|
||||
if (br > 1000) {
|
||||
@@ -478,9 +484,9 @@
|
||||
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
|
||||
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
|
||||
|
||||
if (s.media_type != 'photo' && parseInt(s.bandwidth)) {
|
||||
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
|
||||
var bw = parseInt(s.bandwidth);
|
||||
if (bw != "Unknown") {
|
||||
if (bw !== "Unknown") {
|
||||
if (bw > 1000) {
|
||||
bw = (bw / 1000).toFixed(1) + ' Mbps';
|
||||
} else {
|
||||
@@ -492,17 +498,19 @@
|
||||
|
||||
// Update the stream progress times
|
||||
$('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format));
|
||||
$('#stream-view-offset-' + key).data('state', s.state);
|
||||
if ($('#stream-view-offset-' + key).data('last_view_offset') != s.view_offset) {
|
||||
$('#stream-view-offset-' + key).data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||
var stream_view_offset = $('#stream-view-offset-' + key);
|
||||
stream_view_offset.data('state', s.state);
|
||||
if (stream_view_offset.data('last_view_offset') !== s.view_offset) {
|
||||
stream_view_offset.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||
}
|
||||
|
||||
// Update the progress bars, percent - 3 because of 3px padding-right
|
||||
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
|
||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||
$('#progress-bar-' + key).data('state', s.state);
|
||||
if ($('#progress-bar-' + key).data('last_view_offset') != s.view_offset) {
|
||||
$('#progress-bar-' + key).data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||
var progress_bar = $('#progress-bar-' + key);
|
||||
progress_bar.data('state', s.state);
|
||||
if (progress_bar.data('last_view_offset') !== s.view_offset) {
|
||||
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||
}
|
||||
|
||||
// Add temporary class so we know which instances are still active
|
||||
@@ -771,13 +779,13 @@
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
if (leftTotal === 0) {
|
||||
$("#recently-added-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
if (leftTotal === leftMax) {
|
||||
$("#recently-added-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
|
@@ -38,20 +38,21 @@ DOCUMENTATION :: END
|
||||
<%!
|
||||
import re
|
||||
|
||||
from plexpy import common, notifiers
|
||||
from plexpy import notifiers
|
||||
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
|
||||
|
||||
# Get audio codec file
|
||||
def af(codec):
|
||||
for pattern, file in common.MEDIA_FLAGS_AUDIO.iteritems():
|
||||
for pattern, file_type in MEDIA_FLAGS_AUDIO.iteritems():
|
||||
if re.match(pattern, codec):
|
||||
return file
|
||||
return file_type
|
||||
return codec
|
||||
|
||||
# Get audio codec file
|
||||
def vf(codec):
|
||||
for pattern, file in common.MEDIA_FLAGS_VIDEO.iteritems():
|
||||
for pattern, file_type in MEDIA_FLAGS_VIDEO.iteritems():
|
||||
if re.match(pattern, codec):
|
||||
return file
|
||||
return file_type
|
||||
return codec
|
||||
|
||||
def br(text):
|
||||
@@ -356,7 +357,7 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Movies in <strong>${data['title']}</strong> collection</span>
|
||||
<span>${MEDIA_TYPE_HEADERS[data['sub_media_type']]} in <strong>${data['title']}</strong> collection</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
|
@@ -28,22 +28,15 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
<%
|
||||
from plexpy.common import MEDIA_TYPE_HEADERS
|
||||
types = ('movie', 'show', 'artist', 'album')
|
||||
headers = {'movie': 'Movies',
|
||||
'show': 'TV Shows',
|
||||
'season': 'Seasons',
|
||||
'episode': 'Episodes',
|
||||
'artist': 'Artists',
|
||||
'album': 'Albums',
|
||||
'track': 'Tracks',
|
||||
}
|
||||
%>
|
||||
% for media_type in types:
|
||||
% if data['results_list'][media_type]:
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>${headers[media_type]} in <strong>${title}</strong> collection</span>
|
||||
<span>${MEDIA_TYPE_HEADERS[media_type]} in <strong>${title}</strong> collection</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
|
@@ -28,9 +28,7 @@ libraries_list_table_options = {
|
||||
$(td).html('<div class="edit-library-toggles">' +
|
||||
'<button class="btn btn-xs btn-warning delete-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||
'<button class="btn btn-xs btn-warning purge-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||
'<input type="checkbox" id="do_notify-' + rowData['section_id'] + '" name="do_notify" value="1" ' + rowData['do_notify'] + '><label class="edit-tooltip" for="do_notify-' + rowData['section_id'] + '" data-toggle="tooltip" title="Toggle Notifications"><i class="fa fa-bell fa-lg fa-fw"></i></label> ' +
|
||||
'<input type="checkbox" id="keep_history-' + rowData['section_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['section_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label> ' +
|
||||
'<input type="checkbox" id="do_notify_created-' + rowData['section_id'] + '" name="do_notify_created" value="1" ' + rowData['do_notify_created'] + '><label class="edit-tooltip" for="do_notify_created-' + rowData['section_id'] + '" data-toggle="tooltip" title="Toggle Recently Added"><i class="fa fa-download fa-lg fa-fw"></i></label> ' +
|
||||
'</div>');
|
||||
},
|
||||
"width": "7%",
|
||||
@@ -258,15 +256,7 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
|
||||
var row = libraries_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var do_notify = 0;
|
||||
var do_notify_created = 0;
|
||||
var keep_history = 0;
|
||||
if ($('#do_notify-' + rowData['section_id']).is(':checked')) {
|
||||
do_notify = 1;
|
||||
}
|
||||
if ($('#do_notify_created-' + rowData['section_id']).is(':checked')) {
|
||||
do_notify_created = 1;
|
||||
}
|
||||
if ($('#keep_history-' + rowData['section_id']).is(':checked')) {
|
||||
keep_history = 1;
|
||||
}
|
||||
@@ -280,8 +270,6 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
|
||||
url: 'edit_library',
|
||||
data: {
|
||||
section_id: rowData['section_id'],
|
||||
do_notify: do_notify,
|
||||
do_notify_created: do_notify_created,
|
||||
keep_history: keep_history,
|
||||
custom_thumb: custom_thumb
|
||||
},
|
||||
|
@@ -45,7 +45,6 @@ users_list_table_options = {
|
||||
$(td).html('<div class="edit-user-toggles">' +
|
||||
'<button class="btn btn-xs btn-warning delete-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||
'<button class="btn btn-xs btn-warning purge-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||
'<input type="checkbox" id="do_notify-' + rowData['user_id'] + '" name="do_notify" value="1" ' + rowData['do_notify'] + '><label class="edit-tooltip" for="do_notify-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle Notifications"><i class="fa fa-bell fa-lg fa-fw"></i></label> ' +
|
||||
'<input type="checkbox" id="keep_history-' + rowData['user_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label> ' +
|
||||
'<input type="checkbox" id="allow_guest-' + rowData['user_id'] + '" name="allow_guest" value="1" ' + rowData['allow_guest'] + '><label class="edit-tooltip" for="allow_guest-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle Guest Access"><i class="fa fa-unlock-alt fa-lg fa-fw"></i></label> ' +
|
||||
'</div>');
|
||||
@@ -284,12 +283,8 @@ $('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > inpu
|
||||
var row = users_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
var do_notify = 0;
|
||||
var keep_history = 0;
|
||||
var allow_guest = 0;
|
||||
if ($('#do_notify-' + rowData['user_id']).is(':checked')) {
|
||||
do_notify = 1;
|
||||
}
|
||||
if ($('#keep_history-' + rowData['user_id']).is(':checked')) {
|
||||
keep_history = 1;
|
||||
}
|
||||
@@ -304,7 +299,6 @@ $('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > inpu
|
||||
data: {
|
||||
user_id: rowData['user_id'],
|
||||
friendly_name: friendly_name,
|
||||
do_notify: do_notify,
|
||||
keep_history: keep_history,
|
||||
allow_guest: allow_guest,
|
||||
thumb: rowData['user_thumb']
|
||||
|
@@ -132,12 +132,9 @@
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
|
||||
<label>Notification Conditions</label>
|
||||
<p class="help-block">
|
||||
Add custom notification conditions.
|
||||
Add custom conditions to filter out notifications.
|
||||
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
||||
</p>
|
||||
<p class="help-block">
|
||||
Note: Conditions are checked after the notification trigger and the notification will only be sent if the condition logic is satisfied.
|
||||
</p>
|
||||
<div id="condition-widget"></div>
|
||||
<input type="hidden" name="custom_conditions" id="custom_conditions" />
|
||||
|
||||
@@ -146,7 +143,8 @@
|
||||
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" required />
|
||||
<div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div>
|
||||
<p class="help-block">
|
||||
Enter the logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
|
||||
Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
|
||||
Leave blank for implicit <span class="inline-pre">and</span> between all conditions.
|
||||
</p>
|
||||
<p class="help-block">
|
||||
Note: Only the keywords <span class="inline-pre">and</span>/<span class="inline-pre">or</span> and brackets <span class="inline-pre">()</span> are supported.
|
||||
|
@@ -63,7 +63,7 @@ DOCUMENTATION :: END
|
||||
<h3 class="text-muted"> </h3>
|
||||
</div>
|
||||
% elif item['media_type'] == 'show':
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}">
|
||||
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||
<div class="dashboard-recent-media-poster">
|
||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="dashboard-recent-media-overlay">
|
||||
|
@@ -918,7 +918,7 @@
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes <span style="color: #eb8600; padding-left: 10px;">[experimental]</span>
|
||||
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes
|
||||
</label>
|
||||
<p class="help-block">Enable if you want Tautulli to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.</p>
|
||||
</div>
|
||||
|
@@ -39,7 +39,7 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data:
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||
%>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
@@ -54,6 +54,11 @@ DOCUMENTATION :: END
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
% if data['current_session']:
|
||||
<div class="col-sm-12 text-muted stream-info-current">
|
||||
<i class="fa fa-exclamation-circle"></i> Current session. Updated stream details below may be delayed.
|
||||
</div>
|
||||
% endif
|
||||
<table class="stream-info" style="margin-top: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -85,8 +90,8 @@ DOCUMENTATION :: END
|
||||
% if data['media_type'] != 'track':
|
||||
<tr>
|
||||
<td>Resolution</td>
|
||||
<td>${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}</td>
|
||||
<td>${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])}</td>
|
||||
<td>${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}</td>
|
||||
<td>${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])}</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
@@ -124,8 +129,8 @@ DOCUMENTATION :: END
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Container</td>
|
||||
<td>${data['stream_container']}</td>
|
||||
<td>${data['container']}</td>
|
||||
<td>${data['stream_container'].upper()}</td>
|
||||
<td>${data['container'].upper()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -144,8 +149,8 @@ DOCUMENTATION :: END
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Codec</td>
|
||||
<td>${data['stream_video_codec']}</td>
|
||||
<td>${data['video_codec']}</td>
|
||||
<td>${data['stream_video_codec'].upper()} ${'(HW)' if data['transcode_hw_encoding'] else ''}</td>
|
||||
<td>${data['video_codec'].upper()} ${'(HW)' if data['transcode_hw_decoding'] else ''}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bitrate</td>
|
||||
@@ -189,8 +194,8 @@ DOCUMENTATION :: END
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Codec</td>
|
||||
<td>${data['stream_audio_codec']}</td>
|
||||
<td>${data['audio_codec']}</td>
|
||||
<td>${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())}</td>
|
||||
<td>${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bitrate</td>
|
||||
@@ -219,8 +224,8 @@ DOCUMENTATION :: END
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Codec</td>
|
||||
<td>${data['stream_subtitle_codec']}</td>
|
||||
<td>${data['subtitle_codec']}</td>
|
||||
<td>${data['stream_subtitle_codec'].upper()}</td>
|
||||
<td>${data['subtitle_codec'].upper()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
2
lib/idna/__init__.py
Normal file
2
lib/idna/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .package_data import __version__
|
||||
from .core import *
|
118
lib/idna/codec.py
Normal file
118
lib/idna/codec.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from .core import encode, decode, alabel, ulabel, IDNAError
|
||||
import codecs
|
||||
import re
|
||||
|
||||
_unicode_dots_re = re.compile(u'[\u002e\u3002\uff0e\uff61]')
|
||||
|
||||
class Codec(codecs.Codec):
|
||||
|
||||
def encode(self, data, errors='strict'):
|
||||
|
||||
if errors != 'strict':
|
||||
raise IDNAError("Unsupported error handling \"{0}\"".format(errors))
|
||||
|
||||
if not data:
|
||||
return "", 0
|
||||
|
||||
return encode(data), len(data)
|
||||
|
||||
def decode(self, data, errors='strict'):
|
||||
|
||||
if errors != 'strict':
|
||||
raise IDNAError("Unsupported error handling \"{0}\"".format(errors))
|
||||
|
||||
if not data:
|
||||
return u"", 0
|
||||
|
||||
return decode(data), len(data)
|
||||
|
||||
class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
|
||||
def _buffer_encode(self, data, errors, final):
|
||||
if errors != 'strict':
|
||||
raise IDNAError("Unsupported error handling \"{0}\"".format(errors))
|
||||
|
||||
if not data:
|
||||
return ("", 0)
|
||||
|
||||
labels = _unicode_dots_re.split(data)
|
||||
trailing_dot = u''
|
||||
if labels:
|
||||
if not labels[-1]:
|
||||
trailing_dot = '.'
|
||||
del labels[-1]
|
||||
elif not final:
|
||||
# Keep potentially unfinished label until the next call
|
||||
del labels[-1]
|
||||
if labels:
|
||||
trailing_dot = '.'
|
||||
|
||||
result = []
|
||||
size = 0
|
||||
for label in labels:
|
||||
result.append(alabel(label))
|
||||
if size:
|
||||
size += 1
|
||||
size += len(label)
|
||||
|
||||
# Join with U+002E
|
||||
result = ".".join(result) + trailing_dot
|
||||
size += len(trailing_dot)
|
||||
return (result, size)
|
||||
|
||||
class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
|
||||
def _buffer_decode(self, data, errors, final):
|
||||
if errors != 'strict':
|
||||
raise IDNAError("Unsupported error handling \"{0}\"".format(errors))
|
||||
|
||||
if not data:
|
||||
return (u"", 0)
|
||||
|
||||
# IDNA allows decoding to operate on Unicode strings, too.
|
||||
if isinstance(data, unicode):
|
||||
labels = _unicode_dots_re.split(data)
|
||||
else:
|
||||
# Must be ASCII string
|
||||
data = str(data)
|
||||
unicode(data, "ascii")
|
||||
labels = data.split(".")
|
||||
|
||||
trailing_dot = u''
|
||||
if labels:
|
||||
if not labels[-1]:
|
||||
trailing_dot = u'.'
|
||||
del labels[-1]
|
||||
elif not final:
|
||||
# Keep potentially unfinished label until the next call
|
||||
del labels[-1]
|
||||
if labels:
|
||||
trailing_dot = u'.'
|
||||
|
||||
result = []
|
||||
size = 0
|
||||
for label in labels:
|
||||
result.append(ulabel(label))
|
||||
if size:
|
||||
size += 1
|
||||
size += len(label)
|
||||
|
||||
result = u".".join(result) + trailing_dot
|
||||
size += len(trailing_dot)
|
||||
return (result, size)
|
||||
|
||||
|
||||
class StreamWriter(Codec, codecs.StreamWriter):
|
||||
pass
|
||||
|
||||
class StreamReader(Codec, codecs.StreamReader):
|
||||
pass
|
||||
|
||||
def getregentry():
|
||||
return codecs.CodecInfo(
|
||||
name='idna',
|
||||
encode=Codec().encode,
|
||||
decode=Codec().decode,
|
||||
incrementalencoder=IncrementalEncoder,
|
||||
incrementaldecoder=IncrementalDecoder,
|
||||
streamwriter=StreamWriter,
|
||||
streamreader=StreamReader,
|
||||
)
|
12
lib/idna/compat.py
Normal file
12
lib/idna/compat.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .core import *
|
||||
from .codec import *
|
||||
|
||||
def ToASCII(label):
|
||||
return encode(label)
|
||||
|
||||
def ToUnicode(label):
|
||||
return decode(label)
|
||||
|
||||
def nameprep(s):
|
||||
raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol")
|
||||
|
387
lib/idna/core.py
Normal file
387
lib/idna/core.py
Normal file
@@ -0,0 +1,387 @@
|
||||
from . import idnadata
|
||||
import bisect
|
||||
import unicodedata
|
||||
import re
|
||||
import sys
|
||||
from .intranges import intranges_contain
|
||||
|
||||
_virama_combining_class = 9
|
||||
_alabel_prefix = b'xn--'
|
||||
_unicode_dots_re = re.compile(u'[\u002e\u3002\uff0e\uff61]')
|
||||
|
||||
if sys.version_info[0] == 3:
|
||||
unicode = str
|
||||
unichr = chr
|
||||
|
||||
class IDNAError(UnicodeError):
|
||||
""" Base exception for all IDNA-encoding related problems """
|
||||
pass
|
||||
|
||||
|
||||
class IDNABidiError(IDNAError):
|
||||
""" Exception when bidirectional requirements are not satisfied """
|
||||
pass
|
||||
|
||||
|
||||
class InvalidCodepoint(IDNAError):
|
||||
""" Exception when a disallowed or unallocated codepoint is used """
|
||||
pass
|
||||
|
||||
|
||||
class InvalidCodepointContext(IDNAError):
|
||||
""" Exception when the codepoint is not valid in the context it is used """
|
||||
pass
|
||||
|
||||
|
||||
def _combining_class(cp):
|
||||
return unicodedata.combining(unichr(cp))
|
||||
|
||||
def _is_script(cp, script):
|
||||
return intranges_contain(ord(cp), idnadata.scripts[script])
|
||||
|
||||
def _punycode(s):
|
||||
return s.encode('punycode')
|
||||
|
||||
def _unot(s):
|
||||
return 'U+{0:04X}'.format(s)
|
||||
|
||||
|
||||
def valid_label_length(label):
|
||||
|
||||
if len(label) > 63:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def valid_string_length(label, trailing_dot):
|
||||
|
||||
if len(label) > (254 if trailing_dot else 253):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_bidi(label, check_ltr=False):
|
||||
|
||||
# Bidi rules should only be applied if string contains RTL characters
|
||||
bidi_label = False
|
||||
for (idx, cp) in enumerate(label, 1):
|
||||
direction = unicodedata.bidirectional(cp)
|
||||
if direction == '':
|
||||
# String likely comes from a newer version of Unicode
|
||||
raise IDNABidiError('Unknown directionality in label {0} at position {1}'.format(repr(label), idx))
|
||||
if direction in ['R', 'AL', 'AN']:
|
||||
bidi_label = True
|
||||
break
|
||||
if not bidi_label and not check_ltr:
|
||||
return True
|
||||
|
||||
# Bidi rule 1
|
||||
direction = unicodedata.bidirectional(label[0])
|
||||
if direction in ['R', 'AL']:
|
||||
rtl = True
|
||||
elif direction == 'L':
|
||||
rtl = False
|
||||
else:
|
||||
raise IDNABidiError('First codepoint in label {0} must be directionality L, R or AL'.format(repr(label)))
|
||||
|
||||
valid_ending = False
|
||||
number_type = False
|
||||
for (idx, cp) in enumerate(label, 1):
|
||||
direction = unicodedata.bidirectional(cp)
|
||||
|
||||
if rtl:
|
||||
# Bidi rule 2
|
||||
if not direction in ['R', 'AL', 'AN', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']:
|
||||
raise IDNABidiError('Invalid direction for codepoint at position {0} in a right-to-left label'.format(idx))
|
||||
# Bidi rule 3
|
||||
if direction in ['R', 'AL', 'EN', 'AN']:
|
||||
valid_ending = True
|
||||
elif direction != 'NSM':
|
||||
valid_ending = False
|
||||
# Bidi rule 4
|
||||
if direction in ['AN', 'EN']:
|
||||
if not number_type:
|
||||
number_type = direction
|
||||
else:
|
||||
if number_type != direction:
|
||||
raise IDNABidiError('Can not mix numeral types in a right-to-left label')
|
||||
else:
|
||||
# Bidi rule 5
|
||||
if not direction in ['L', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']:
|
||||
raise IDNABidiError('Invalid direction for codepoint at position {0} in a left-to-right label'.format(idx))
|
||||
# Bidi rule 6
|
||||
if direction in ['L', 'EN']:
|
||||
valid_ending = True
|
||||
elif direction != 'NSM':
|
||||
valid_ending = False
|
||||
|
||||
if not valid_ending:
|
||||
raise IDNABidiError('Label ends with illegal codepoint directionality')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_initial_combiner(label):
|
||||
|
||||
if unicodedata.category(label[0])[0] == 'M':
|
||||
raise IDNAError('Label begins with an illegal combining character')
|
||||
return True
|
||||
|
||||
|
||||
def check_hyphen_ok(label):
|
||||
|
||||
if label[2:4] == '--':
|
||||
raise IDNAError('Label has disallowed hyphens in 3rd and 4th position')
|
||||
if label[0] == '-' or label[-1] == '-':
|
||||
raise IDNAError('Label must not start or end with a hyphen')
|
||||
return True
|
||||
|
||||
|
||||
def check_nfc(label):
|
||||
|
||||
if unicodedata.normalize('NFC', label) != label:
|
||||
raise IDNAError('Label must be in Normalization Form C')
|
||||
|
||||
|
||||
def valid_contextj(label, pos):
|
||||
|
||||
cp_value = ord(label[pos])
|
||||
|
||||
if cp_value == 0x200c:
|
||||
|
||||
if pos > 0:
|
||||
if _combining_class(ord(label[pos - 1])) == _virama_combining_class:
|
||||
return True
|
||||
|
||||
ok = False
|
||||
for i in range(pos-1, -1, -1):
|
||||
joining_type = idnadata.joining_types.get(ord(label[i]))
|
||||
if joining_type == ord('T'):
|
||||
continue
|
||||
if joining_type in [ord('L'), ord('D')]:
|
||||
ok = True
|
||||
break
|
||||
|
||||
if not ok:
|
||||
return False
|
||||
|
||||
ok = False
|
||||
for i in range(pos+1, len(label)):
|
||||
joining_type = idnadata.joining_types.get(ord(label[i]))
|
||||
if joining_type == ord('T'):
|
||||
continue
|
||||
if joining_type in [ord('R'), ord('D')]:
|
||||
ok = True
|
||||
break
|
||||
return ok
|
||||
|
||||
if cp_value == 0x200d:
|
||||
|
||||
if pos > 0:
|
||||
if _combining_class(ord(label[pos - 1])) == _virama_combining_class:
|
||||
return True
|
||||
return False
|
||||
|
||||
else:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def valid_contexto(label, pos, exception=False):
|
||||
|
||||
cp_value = ord(label[pos])
|
||||
|
||||
if cp_value == 0x00b7:
|
||||
if 0 < pos < len(label)-1:
|
||||
if ord(label[pos - 1]) == 0x006c and ord(label[pos + 1]) == 0x006c:
|
||||
return True
|
||||
return False
|
||||
|
||||
elif cp_value == 0x0375:
|
||||
if pos < len(label)-1 and len(label) > 1:
|
||||
return _is_script(label[pos + 1], 'Greek')
|
||||
return False
|
||||
|
||||
elif cp_value == 0x05f3 or cp_value == 0x05f4:
|
||||
if pos > 0:
|
||||
return _is_script(label[pos - 1], 'Hebrew')
|
||||
return False
|
||||
|
||||
elif cp_value == 0x30fb:
|
||||
for cp in label:
|
||||
if cp == u'\u30fb':
|
||||
continue
|
||||
if _is_script(cp, 'Hiragana') or _is_script(cp, 'Katakana') or _is_script(cp, 'Han'):
|
||||
return True
|
||||
return False
|
||||
|
||||
elif 0x660 <= cp_value <= 0x669:
|
||||
for cp in label:
|
||||
if 0x6f0 <= ord(cp) <= 0x06f9:
|
||||
return False
|
||||
return True
|
||||
|
||||
elif 0x6f0 <= cp_value <= 0x6f9:
|
||||
for cp in label:
|
||||
if 0x660 <= ord(cp) <= 0x0669:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_label(label):
|
||||
|
||||
if isinstance(label, (bytes, bytearray)):
|
||||
label = label.decode('utf-8')
|
||||
if len(label) == 0:
|
||||
raise IDNAError('Empty Label')
|
||||
|
||||
check_nfc(label)
|
||||
check_hyphen_ok(label)
|
||||
check_initial_combiner(label)
|
||||
|
||||
for (pos, cp) in enumerate(label):
|
||||
cp_value = ord(cp)
|
||||
if intranges_contain(cp_value, idnadata.codepoint_classes['PVALID']):
|
||||
continue
|
||||
elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']):
|
||||
if not valid_contextj(label, pos):
|
||||
raise InvalidCodepointContext('Joiner {0} not allowed at position {1} in {2}'.format(_unot(cp_value), pos+1, repr(label)))
|
||||
elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']):
|
||||
if not valid_contexto(label, pos):
|
||||
raise InvalidCodepointContext('Codepoint {0} not allowed at position {1} in {2}'.format(_unot(cp_value), pos+1, repr(label)))
|
||||
else:
|
||||
raise InvalidCodepoint('Codepoint {0} at position {1} of {2} not allowed'.format(_unot(cp_value), pos+1, repr(label)))
|
||||
|
||||
check_bidi(label)
|
||||
|
||||
|
||||
def alabel(label):
|
||||
|
||||
try:
|
||||
label = label.encode('ascii')
|
||||
try:
|
||||
ulabel(label)
|
||||
except IDNAError:
|
||||
raise IDNAError('The label {0} is not a valid A-label'.format(label))
|
||||
if not valid_label_length(label):
|
||||
raise IDNAError('Label too long')
|
||||
return label
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
|
||||
if not label:
|
||||
raise IDNAError('No Input')
|
||||
|
||||
label = unicode(label)
|
||||
check_label(label)
|
||||
label = _punycode(label)
|
||||
label = _alabel_prefix + label
|
||||
|
||||
if not valid_label_length(label):
|
||||
raise IDNAError('Label too long')
|
||||
|
||||
return label
|
||||
|
||||
|
||||
def ulabel(label):
|
||||
|
||||
if not isinstance(label, (bytes, bytearray)):
|
||||
try:
|
||||
label = label.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
check_label(label)
|
||||
return label
|
||||
|
||||
label = label.lower()
|
||||
if label.startswith(_alabel_prefix):
|
||||
label = label[len(_alabel_prefix):]
|
||||
else:
|
||||
check_label(label)
|
||||
return label.decode('ascii')
|
||||
|
||||
label = label.decode('punycode')
|
||||
check_label(label)
|
||||
return label
|
||||
|
||||
|
||||
def uts46_remap(domain, std3_rules=True, transitional=False):
|
||||
"""Re-map the characters in the string according to UTS46 processing."""
|
||||
from .uts46data import uts46data
|
||||
output = u""
|
||||
try:
|
||||
for pos, char in enumerate(domain):
|
||||
code_point = ord(char)
|
||||
uts46row = uts46data[code_point if code_point < 256 else
|
||||
bisect.bisect_left(uts46data, (code_point, "Z")) - 1]
|
||||
status = uts46row[1]
|
||||
replacement = uts46row[2] if len(uts46row) == 3 else None
|
||||
if (status == "V" or
|
||||
(status == "D" and not transitional) or
|
||||
(status == "3" and std3_rules and replacement is None)):
|
||||
output += char
|
||||
elif replacement is not None and (status == "M" or
|
||||
(status == "3" and std3_rules) or
|
||||
(status == "D" and transitional)):
|
||||
output += replacement
|
||||
elif status != "I":
|
||||
raise IndexError()
|
||||
return unicodedata.normalize("NFC", output)
|
||||
except IndexError:
|
||||
raise InvalidCodepoint(
|
||||
"Codepoint {0} not allowed at position {1} in {2}".format(
|
||||
_unot(code_point), pos + 1, repr(domain)))
|
||||
|
||||
|
||||
def encode(s, strict=False, uts46=False, std3_rules=False, transitional=False):
|
||||
|
||||
if isinstance(s, (bytes, bytearray)):
|
||||
s = s.decode("ascii")
|
||||
if uts46:
|
||||
s = uts46_remap(s, std3_rules, transitional)
|
||||
trailing_dot = False
|
||||
result = []
|
||||
if strict:
|
||||
labels = s.split('.')
|
||||
else:
|
||||
labels = _unicode_dots_re.split(s)
|
||||
while labels and not labels[0]:
|
||||
del labels[0]
|
||||
if not labels:
|
||||
raise IDNAError('Empty domain')
|
||||
if labels[-1] == '':
|
||||
del labels[-1]
|
||||
trailing_dot = True
|
||||
for label in labels:
|
||||
result.append(alabel(label))
|
||||
if trailing_dot:
|
||||
result.append(b'')
|
||||
s = b'.'.join(result)
|
||||
if not valid_string_length(s, trailing_dot):
|
||||
raise IDNAError('Domain too long')
|
||||
return s
|
||||
|
||||
|
||||
def decode(s, strict=False, uts46=False, std3_rules=False):
|
||||
|
||||
if isinstance(s, (bytes, bytearray)):
|
||||
s = s.decode("ascii")
|
||||
if uts46:
|
||||
s = uts46_remap(s, std3_rules, False)
|
||||
trailing_dot = False
|
||||
result = []
|
||||
if not strict:
|
||||
labels = _unicode_dots_re.split(s)
|
||||
else:
|
||||
labels = s.split(u'.')
|
||||
while labels and not labels[0]:
|
||||
del labels[0]
|
||||
if not labels:
|
||||
raise IDNAError('Empty domain')
|
||||
if not labels[-1]:
|
||||
del labels[-1]
|
||||
trailing_dot = True
|
||||
for label in labels:
|
||||
result.append(ulabel(label))
|
||||
if trailing_dot:
|
||||
result.append(u'')
|
||||
return u'.'.join(result)
|
1585
lib/idna/idnadata.py
Normal file
1585
lib/idna/idnadata.py
Normal file
File diff suppressed because it is too large
Load Diff
53
lib/idna/intranges.py
Normal file
53
lib/idna/intranges.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Given a list of integers, made up of (hopefully) a small number of long runs
|
||||
of consecutive integers, compute a representation of the form
|
||||
((start1, end1), (start2, end2) ...). Then answer the question "was x present
|
||||
in the original list?" in time O(log(# runs)).
|
||||
"""
|
||||
|
||||
import bisect
|
||||
|
||||
def intranges_from_list(list_):
|
||||
"""Represent a list of integers as a sequence of ranges:
|
||||
((start_0, end_0), (start_1, end_1), ...), such that the original
|
||||
integers are exactly those x such that start_i <= x < end_i for some i.
|
||||
|
||||
Ranges are encoded as single integers (start << 32 | end), not as tuples.
|
||||
"""
|
||||
|
||||
sorted_list = sorted(list_)
|
||||
ranges = []
|
||||
last_write = -1
|
||||
for i in range(len(sorted_list)):
|
||||
if i+1 < len(sorted_list):
|
||||
if sorted_list[i] == sorted_list[i+1]-1:
|
||||
continue
|
||||
current_range = sorted_list[last_write+1:i+1]
|
||||
ranges.append(_encode_range(current_range[0], current_range[-1] + 1))
|
||||
last_write = i
|
||||
|
||||
return tuple(ranges)
|
||||
|
||||
def _encode_range(start, end):
|
||||
return (start << 32) | end
|
||||
|
||||
def _decode_range(r):
|
||||
return (r >> 32), (r & ((1 << 32) - 1))
|
||||
|
||||
|
||||
def intranges_contain(int_, ranges):
|
||||
"""Determine if `int_` falls into one of the ranges in `ranges`."""
|
||||
tuple_ = _encode_range(int_, 0)
|
||||
pos = bisect.bisect_left(ranges, tuple_)
|
||||
# we could be immediately ahead of a tuple (start, end)
|
||||
# with start < int_ <= end
|
||||
if pos > 0:
|
||||
left, right = _decode_range(ranges[pos-1])
|
||||
if left <= int_ < right:
|
||||
return True
|
||||
# or we could be immediately behind a tuple (int_, end)
|
||||
if pos < len(ranges):
|
||||
left, _ = _decode_range(ranges[pos])
|
||||
if left == int_:
|
||||
return True
|
||||
return False
|
2
lib/idna/package_data.py
Normal file
2
lib/idna/package_data.py
Normal file
@@ -0,0 +1,2 @@
|
||||
__version__ = '2.6'
|
||||
|
7634
lib/idna/uts46data.py
Normal file
7634
lib/idna/uts46data.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -443,6 +443,7 @@ def dbcheck():
|
||||
'transcode_protocol TEXT, transcode_container TEXT, '
|
||||
'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,'
|
||||
'transcode_width INTEGER, transcode_height INTEGER, '
|
||||
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
|
||||
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
|
||||
'synced_version INTEGER, synced_version_profile TEXT, '
|
||||
'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, write_attempts INTEGER DEFAULT 0, '
|
||||
@@ -468,8 +469,9 @@ def dbcheck():
|
||||
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
|
||||
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
|
||||
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
|
||||
'transcode_hw_requested INTEGER, transcode_hw_full_pipeline INTEGER, transcode_hw_decode TEXT, '
|
||||
'transcode_hw_decode_title TEXT, transcode_hw_encode TEXT, transcode_hw_encode_title TEXT, '
|
||||
'transcode_hw_requested INTEGER, transcode_hw_full_pipeline INTEGER, '
|
||||
'transcode_hw_decode TEXT, transcode_hw_decode_title TEXT, transcode_hw_decoding INTEGER, '
|
||||
'transcode_hw_encode TEXT, transcode_hw_encode_title TEXT, transcode_hw_encoding INTEGER, '
|
||||
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
|
||||
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
|
||||
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
|
||||
@@ -917,6 +919,18 @@ def dbcheck():
|
||||
'ALTER TABLE sessions ADD COLUMN optimized_version_title TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT transcode_hw_decoding FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN transcode_hw_decoding INTEGER'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN transcode_hw_encoding INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade session_history table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT reference_id FROM session_history')
|
||||
@@ -1159,6 +1173,43 @@ def dbcheck():
|
||||
'ALTER TABLE session_history_media_info ADD COLUMN optimized_version_title TEXT '
|
||||
)
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT transcode_hw_decoding FROM session_history_media_info')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table session_history_media_info.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_media_info ADD COLUMN transcode_hw_decoding INTEGER '
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE session_history_media_info ADD COLUMN transcode_hw_encoding INTEGER '
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
|
||||
)
|
||||
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
||||
'WHERE stream_container IS NULL').fetchall()
|
||||
if len(result) > 0:
|
||||
logger.debug(u"Altering database. Removing NULL values from session_history_media_info table.")
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET stream_container = "" WHERE stream_container IS NULL '
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET stream_video_codec = "" WHERE stream_video_codec IS NULL '
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET stream_audio_codec = "" WHERE stream_audio_codec IS NULL '
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET stream_subtitle_codec = "" WHERE stream_subtitle_codec IS NULL '
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
logger.warn(u"Unable to remove NULL values from session_history_media_info table.")
|
||||
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT do_notify FROM users')
|
||||
@@ -1340,8 +1391,8 @@ def dbcheck():
|
||||
|
||||
# Upgrade library_sections table from earlier versions (remove duplicated libraries)
|
||||
try:
|
||||
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""')
|
||||
if result.rowcount > 0:
|
||||
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""').fetchall()
|
||||
if len(result) > 0:
|
||||
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
|
||||
c_db.execute(
|
||||
'DELETE FROM library_sections WHERE server_id = ""'
|
||||
|
@@ -14,7 +14,7 @@
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import threading
|
||||
import os
|
||||
import time
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
@@ -26,7 +26,6 @@ import datafactory
|
||||
import helpers
|
||||
import logger
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import pmsconnect
|
||||
|
||||
|
||||
@@ -34,6 +33,7 @@ ACTIVITY_SCHED = BackgroundScheduler()
|
||||
|
||||
RECENTLY_ADDED_QUEUE = {}
|
||||
|
||||
|
||||
class ActivityHandler(object):
|
||||
|
||||
def __init__(self, timeline):
|
||||
@@ -75,9 +75,12 @@ class ActivityHandler(object):
|
||||
monitor_proc.write_session(session=session, notify=False)
|
||||
|
||||
def on_start(self):
|
||||
if self.is_valid_session() and self.get_live_session():
|
||||
if self.is_valid_session():
|
||||
session = self.get_live_session()
|
||||
|
||||
|
||||
if not session:
|
||||
return
|
||||
|
||||
# Some DLNA clients create a new session temporarily when browsing the library
|
||||
# Wait and get session again to make sure it is an actual session
|
||||
if session['platform'] == 'DLNA':
|
||||
@@ -124,6 +127,7 @@ class ActivityHandler(object):
|
||||
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (str(self.get_session_key()), str(self.get_rating_key())))
|
||||
ap.delete_session(session_key=self.get_session_key())
|
||||
delete_metadata_cache(self.get_session_key())
|
||||
|
||||
def on_pause(self, still_paused=False):
|
||||
if self.is_valid_session():
|
||||
@@ -226,9 +230,11 @@ class ActivityHandler(object):
|
||||
# Update the session state and viewOffset
|
||||
if this_state == 'playing':
|
||||
# Update the session in our temp session table
|
||||
session = self.get_live_session()
|
||||
if session:
|
||||
self.update_db_session(session=session)
|
||||
# if the last set temporary stopped time exceeds 15 seconds
|
||||
if int(time.time()) - db_session['stopped'] > 60:
|
||||
session = self.get_live_session()
|
||||
if session:
|
||||
self.update_db_session(session=session)
|
||||
|
||||
# Start our state checks
|
||||
if this_state != last_state:
|
||||
@@ -436,12 +442,12 @@ def force_stop_stream(session_key):
|
||||
ap.delete_session(session_key=session_key)
|
||||
|
||||
else:
|
||||
sessions['write_attempts'] += 1
|
||||
session['write_attempts'] += 1
|
||||
|
||||
if sessions['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||
if session['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||
logger.warn(u"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again in 30 seconds. Write attempt %s."
|
||||
% (sessions['session_key'], sessions['rating_key'], str(sessions['write_attempts'])))
|
||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||
ap.increment_write_attempts(session_key=session_key)
|
||||
|
||||
# Reschedule for 30 seconds later
|
||||
@@ -449,12 +455,13 @@ def force_stop_stream(session_key):
|
||||
args=[session_key], seconds=30)
|
||||
|
||||
else:
|
||||
logger.warn(u"Tautulli Monitor :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
logger.warn(u"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||
"Removing session from the database. Write attempt %s."
|
||||
% (sessions['session_key'], sessions['rating_key'], str(sessions['write_attempts'])))
|
||||
logger.info(u"Tautulli Monitor :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (sessions['session_key'], sessions['rating_key']))
|
||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (session['session_key'], session['rating_key']))
|
||||
ap.delete_session(session_key=session_key)
|
||||
delete_metadata_cache(session_key)
|
||||
|
||||
|
||||
def clear_recently_added_queue(rating_key):
|
||||
@@ -519,3 +526,11 @@ def on_created(rating_key, **kwargs):
|
||||
|
||||
else:
|
||||
logger.error(u"Tautulli TimelineHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
|
||||
|
||||
|
||||
def delete_metadata_cache(session_key):
|
||||
try:
|
||||
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % session_key))
|
||||
except IOError as e:
|
||||
logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
|
||||
% (session_key, e))
|
||||
|
@@ -58,7 +58,7 @@ class ActivityProcessor(object):
|
||||
'grandparent_thumb': session.get('grandparent_thumb', ''),
|
||||
'year': session.get('year', ''),
|
||||
'friendly_name': session.get('friendly_name', ''),
|
||||
#'ip_address': session.get('ip_address', ''),
|
||||
'ip_address': session.get('ip_address', ''),
|
||||
'player': session.get('player', ''),
|
||||
'platform': session.get('platform', ''),
|
||||
'parent_rating_key': session.get('parent_rating_key', ''),
|
||||
@@ -90,6 +90,8 @@ class ActivityProcessor(object):
|
||||
'transcode_audio_channels': session.get('transcode_audio_channels', ''),
|
||||
'transcode_width': session.get('stream_video_width', ''),
|
||||
'transcode_height': session.get('stream_video_height', ''),
|
||||
'transcode_hw_decoding': session.get('transcode_hw_decoding', ''),
|
||||
'transcode_hw_encoding': session.get('transcode_hw_encoding', ''),
|
||||
'synced_version': session.get('synced_version', ''),
|
||||
'synced_version_profile': session.get('synced_version_profile', ''),
|
||||
'optimized_version': session.get('optimized_version', ''),
|
||||
@@ -117,10 +119,6 @@ class ActivityProcessor(object):
|
||||
'stopped': int(time.time())
|
||||
}
|
||||
|
||||
# Add ip_address back into values
|
||||
if session['ip_address']:
|
||||
values.update({'ip_address': session.get('ip_address', 'N/A')})
|
||||
|
||||
keys = {'session_key': session.get('session_key', ''),
|
||||
'rating_key': session.get('rating_key', '')}
|
||||
|
||||
@@ -129,7 +127,6 @@ class ActivityProcessor(object):
|
||||
if result == 'insert':
|
||||
# Check if any notification agents have notifications enabled
|
||||
if notify:
|
||||
values.update({'ip_address': session.get('ip_address', 'N/A')})
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': values, 'notify_action': 'on_play'})
|
||||
|
||||
# If it's our first write then time stamp it.
|
||||
@@ -324,6 +321,7 @@ class ActivityProcessor(object):
|
||||
'audio_codec': session['audio_codec'],
|
||||
'audio_bitrate': session['audio_bitrate'],
|
||||
'audio_channels': session['audio_channels'],
|
||||
'subtitle_codec': session['subtitle_codec'],
|
||||
'transcode_protocol': session['transcode_protocol'],
|
||||
'transcode_container': session['transcode_container'],
|
||||
'transcode_video_codec': session['transcode_video_codec'],
|
||||
@@ -333,9 +331,11 @@ class ActivityProcessor(object):
|
||||
'transcode_height': session['transcode_height'],
|
||||
'transcode_hw_requested': session['transcode_hw_requested'],
|
||||
'transcode_hw_full_pipeline': session['transcode_hw_full_pipeline'],
|
||||
'transcode_hw_decoding': session['transcode_hw_decoding'],
|
||||
'transcode_hw_decode': session['transcode_hw_decode'],
|
||||
'transcode_hw_encode': session['transcode_hw_encode'],
|
||||
'transcode_hw_decode_title': session['transcode_hw_decode_title'],
|
||||
'transcode_hw_encoding': session['transcode_hw_encoding'],
|
||||
'transcode_hw_encode': session['transcode_hw_encode'],
|
||||
'transcode_hw_encode_title': session['transcode_hw_encode_title'],
|
||||
'stream_container': session['stream_container'],
|
||||
'stream_container_decision': session['stream_container_decision'],
|
||||
@@ -460,7 +460,7 @@ class ActivityProcessor(object):
|
||||
if str(session_key).isdigit():
|
||||
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
||||
|
||||
def set_session_last_paused(self, session_key=None, timestamp=None ):
|
||||
def set_session_last_paused(self, session_key=None, timestamp=None):
|
||||
if str(session_key).isdigit():
|
||||
result = self.db.select('SELECT last_paused, paused_counter '
|
||||
'FROM sessions '
|
||||
@@ -469,7 +469,7 @@ class ActivityProcessor(object):
|
||||
paused_counter = None
|
||||
for session in result:
|
||||
if session['last_paused']:
|
||||
paused_offset = timestamp - int(session['last_paused'])
|
||||
paused_offset = int(time.time()) - int(session['last_paused'])
|
||||
if session['paused_counter']:
|
||||
paused_counter = int(session['paused_counter']) + int(paused_offset)
|
||||
else:
|
||||
|
@@ -35,6 +35,8 @@ import database
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import users
|
||||
|
||||
|
||||
@@ -397,6 +399,50 @@ class API2:
|
||||
|
||||
return
|
||||
|
||||
def notify(self, notifier_id='', subject='Tautulli', body='Test notification', **kwargs):
|
||||
""" Send a notification using Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
notifier_id (int): The ID number of the notification agent
|
||||
subject (str): The subject of the message
|
||||
body (str): The body of the message
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
if not notifier_id:
|
||||
self._api_msg = 'Notification failed: no notifier id provided.'
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
notifier = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||
|
||||
if not notifier:
|
||||
self._api_msg = 'Notification failed: invalid notifier_id provided %s.' % notifier_id
|
||||
self._api_result_type = 'error'
|
||||
return
|
||||
|
||||
logger.api_debug(u'Tautulli APIv2 :: Sending notification.')
|
||||
success = notification_handler.notify(notifier_id=notifier_id,
|
||||
notify_action='api',
|
||||
subject=subject,
|
||||
body=body,
|
||||
**kwargs)
|
||||
|
||||
if success:
|
||||
self._api_msg = 'Notification sent.'
|
||||
self._api_result_type = 'success'
|
||||
else:
|
||||
self._api_msg = 'Notification failed.'
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return
|
||||
|
||||
def _api_make_md(self):
|
||||
""" Tries to make a API.md to simplify the api docs. """
|
||||
|
||||
@@ -581,8 +627,8 @@ General optional parameters:
|
||||
if isinstance(result, (dict, list)):
|
||||
ret = result
|
||||
else:
|
||||
raise
|
||||
except:
|
||||
raise Exception
|
||||
except Exception:
|
||||
try:
|
||||
ret = json.loads(result)
|
||||
except (ValueError, TypeError):
|
||||
|
285
plexpy/common.py
285
plexpy/common.py
@@ -32,130 +32,176 @@ DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
||||
DEFAULT_ART = "interfaces/default/images/art.png"
|
||||
|
||||
PLATFORM_NAME_OVERRIDES = {'Konvergo': 'Plex Media Player',
|
||||
'Mystery 3': 'Playstation 3',
|
||||
'Mystery 4': 'Playstation 4',
|
||||
'Mystery 5': 'Xbox 360',
|
||||
'WebMAF': 'Playstation 4'
|
||||
}
|
||||
MEDIA_TYPE_HEADERS = {
|
||||
'movie': 'Movies',
|
||||
'show': 'TV Shows',
|
||||
'season': 'Seasons',
|
||||
'episode': 'Episodes',
|
||||
'artist': 'Artists',
|
||||
'album': 'Albums',
|
||||
'track': 'Tracks',
|
||||
}
|
||||
|
||||
PMS_PLATFORM_NAME_OVERRIDES = {'MacOSX': 'Mac'
|
||||
}
|
||||
PLATFORM_NAME_OVERRIDES = {
|
||||
'Konvergo': 'Plex Media Player',
|
||||
'Mystery 3': 'Playstation 3',
|
||||
'Mystery 4': 'Playstation 4',
|
||||
'Mystery 5': 'Xbox 360',
|
||||
'WebMAF': 'Playstation 4'
|
||||
}
|
||||
|
||||
PLATFORM_NAMES = {'android': 'android',
|
||||
'apple tv': 'atv',
|
||||
'chrome': 'chrome',
|
||||
'chromecast': 'chromecast',
|
||||
'dlna': 'dlna',
|
||||
'firefox': 'firefox',
|
||||
'internet explorer': 'ie',
|
||||
'ios': 'ios',
|
||||
'ipad': 'ios',
|
||||
'iphone': 'ios',
|
||||
'kodi': 'kodi',
|
||||
'linux': 'linux',
|
||||
'nexus': 'android',
|
||||
'macos': 'macos',
|
||||
'microsoft edge': 'msedge',
|
||||
'opera': 'opera',
|
||||
'osx': 'macos',
|
||||
'playstation': 'playstation',
|
||||
'plex home theater': 'plex',
|
||||
'plex media player': 'plex',
|
||||
'plexamp': 'plexamp',
|
||||
'plextogether': 'synclounge',
|
||||
'roku': 'roku',
|
||||
'safari': 'safari',
|
||||
'samsung': 'samsung',
|
||||
'synclounge': 'synclounge',
|
||||
'tivo': 'tivo',
|
||||
'tvos': 'atv',
|
||||
'vizio': 'opera',
|
||||
'wiiu': 'wiiu',
|
||||
'windows': 'windows',
|
||||
'windows phone': 'wp',
|
||||
'xbmc': 'xbmc',
|
||||
'xbox': 'xbox'
|
||||
}
|
||||
PMS_PLATFORM_NAME_OVERRIDES = {
|
||||
'MacOSX': 'Mac'
|
||||
}
|
||||
|
||||
PLATFORM_NAMES = {
|
||||
'android': 'android',
|
||||
'apple tv': 'atv',
|
||||
'chrome': 'chrome',
|
||||
'chromecast': 'chromecast',
|
||||
'dlna': 'dlna',
|
||||
'firefox': 'firefox',
|
||||
'internet explorer': 'ie',
|
||||
'ios': 'ios',
|
||||
'ipad': 'ios',
|
||||
'iphone': 'ios',
|
||||
'kodi': 'kodi',
|
||||
'linux': 'linux',
|
||||
'nexus': 'android',
|
||||
'macos': 'macos',
|
||||
'microsoft edge': 'msedge',
|
||||
'opera': 'opera',
|
||||
'osx': 'macos',
|
||||
'playstation': 'playstation',
|
||||
'plex home theater': 'plex',
|
||||
'plex media player': 'plex',
|
||||
'plexamp': 'plexamp',
|
||||
'plextogether': 'synclounge',
|
||||
'roku': 'roku',
|
||||
'safari': 'safari',
|
||||
'samsung': 'samsung',
|
||||
'synclounge': 'synclounge',
|
||||
'tivo': 'tivo',
|
||||
'tvos': 'atv',
|
||||
'vizio': 'opera',
|
||||
'wiiu': 'wiiu',
|
||||
'windows': 'windows',
|
||||
'windows phone': 'wp',
|
||||
'xbmc': 'xbmc',
|
||||
'xbox': 'xbox'
|
||||
}
|
||||
PLATFORM_NAMES = OrderedDict(sorted(PLATFORM_NAMES.items(), key=lambda k: k[0], reverse=True))
|
||||
|
||||
MEDIA_FLAGS_AUDIO = {'ac.?3': 'dolby_digital',
|
||||
'truehd': 'dolby_truehd',
|
||||
'(dca|dta)': 'dts',
|
||||
'dts(hd_|-hd|-)?ma': 'dca-ma',
|
||||
'vorbis': 'ogg'
|
||||
}
|
||||
MEDIA_FLAGS_VIDEO = {'avc1': 'h264',
|
||||
'wmv(1|2)': 'wmv',
|
||||
'wmv3': 'wmvhd'
|
||||
}
|
||||
MEDIA_FLAGS_AUDIO = {
|
||||
'ac.?3': 'dolby_digital',
|
||||
'truehd': 'dolby_truehd',
|
||||
'(dca|dta)': 'dts',
|
||||
'dts(hd_|-hd|-)?ma': 'dca-ma',
|
||||
'vorbis': 'ogg'
|
||||
}
|
||||
MEDIA_FLAGS_VIDEO = {
|
||||
'avc1': 'h264',
|
||||
'wmv(1|2)': 'wmv',
|
||||
'wmv3': 'wmvhd'
|
||||
}
|
||||
|
||||
AUDIO_CODEC_OVERRIDES = {'truehd': 'TrueHD'}
|
||||
AUDIO_CODEC_OVERRIDES = {
|
||||
'truehd': 'TrueHD'
|
||||
}
|
||||
|
||||
VIDEO_RESOLUTION_OVERRIDES = {'sd': 'SD',
|
||||
'480': '480p',
|
||||
'540': '540p',
|
||||
'576': '576p',
|
||||
'720': '720p',
|
||||
'1080': '1080p',
|
||||
'4k': '4k'
|
||||
}
|
||||
VIDEO_RESOLUTION_OVERRIDES = {
|
||||
'sd': 'SD',
|
||||
'480': '480p',
|
||||
'540': '540p',
|
||||
'576': '576p',
|
||||
'720': '720p',
|
||||
'1080': '1080p',
|
||||
'4k': '4k'
|
||||
}
|
||||
|
||||
AUDIO_CHANNELS = {'1': 'Mono',
|
||||
'2': 'Stereo',
|
||||
'3': '2.1',
|
||||
'4': '3.1',
|
||||
'6': '5.1',
|
||||
'7': '6.1',
|
||||
'8': '7.1'
|
||||
}
|
||||
AUDIO_CHANNELS = {
|
||||
'1': 'Mono',
|
||||
'2': 'Stereo',
|
||||
'3': '2.1',
|
||||
'4': '3.1',
|
||||
'6': '5.1',
|
||||
'7': '6.1',
|
||||
'8': '7.1'
|
||||
}
|
||||
|
||||
VIDEO_QUALITY_PROFILES = {20000: '20 Mbps 1080p',
|
||||
12000: '12 Mbps 1080p',
|
||||
10000: '10 Mbps 1080p',
|
||||
8000: '8 Mbps 1080p',
|
||||
4000: '4 Mbps 720p',
|
||||
3000: '3 Mbps 720p',
|
||||
2000: '2 Mbps 720p',
|
||||
1500: '1.5 Mbps 480p',
|
||||
720: '0.7 Mbps 328p',
|
||||
320: '0.3 Mbps 240p',
|
||||
208: '0.2 Mbps 160p',
|
||||
96: '0.096 Mbps',
|
||||
64: '0.064 Mbps'
|
||||
}
|
||||
VIDEO_QUALITY_PROFILES = {
|
||||
20000: '20 Mbps 1080p',
|
||||
12000: '12 Mbps 1080p',
|
||||
10000: '10 Mbps 1080p',
|
||||
8000: '8 Mbps 1080p',
|
||||
4000: '4 Mbps 720p',
|
||||
3000: '3 Mbps 720p',
|
||||
2000: '2 Mbps 720p',
|
||||
1500: '1.5 Mbps 480p',
|
||||
720: '0.7 Mbps 328p',
|
||||
320: '0.3 Mbps 240p',
|
||||
208: '0.2 Mbps 160p',
|
||||
96: '0.096 Mbps',
|
||||
64: '0.064 Mbps'
|
||||
}
|
||||
VIDEO_QUALITY_PROFILES = OrderedDict(sorted(VIDEO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True))
|
||||
|
||||
AUDIO_QUALITY_PROFILES = {512: '512 kbps',
|
||||
320: '320 kbps',
|
||||
256: '256 kbps',
|
||||
192: '192 kbps',
|
||||
128: '128 kbps',
|
||||
96: '96 kbps'
|
||||
}
|
||||
AUDIO_QUALITY_PROFILES = {
|
||||
512: '512 kbps',
|
||||
320: '320 kbps',
|
||||
256: '256 kbps',
|
||||
192: '192 kbps',
|
||||
128: '128 kbps',
|
||||
96: '96 kbps'
|
||||
}
|
||||
AUDIO_QUALITY_PROFILES = OrderedDict(sorted(AUDIO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True))
|
||||
|
||||
SCHEDULER_LIST = ['Check GitHub for updates',
|
||||
'Check for active sessions',
|
||||
'Check for recently added items',
|
||||
'Check for Plex updates',
|
||||
'Check for Plex remote access',
|
||||
'Check server response',
|
||||
'Refresh users list',
|
||||
'Refresh libraries list',
|
||||
'Refresh Plex server URLs',
|
||||
'Backup Tautulli database',
|
||||
'Backup Tautulli config'
|
||||
]
|
||||
HW_DECODERS = [
|
||||
'dxva2',
|
||||
'videotoolbox',
|
||||
'mediacodecndk',
|
||||
'vaapi'
|
||||
]
|
||||
HW_ENCODERS = [
|
||||
'qsv',
|
||||
'nvenc',
|
||||
'mf',
|
||||
'videotoolbox',
|
||||
'mediacodecndk',
|
||||
'vaapi',
|
||||
'nvenc'
|
||||
]
|
||||
|
||||
SCHEDULER_LIST = [
|
||||
'Check GitHub for updates',
|
||||
'Check for active sessions',
|
||||
'Check for recently added items',
|
||||
'Check for Plex updates',
|
||||
'Check for Plex remote access',
|
||||
'Check server response',
|
||||
'Refresh users list',
|
||||
'Refresh libraries list',
|
||||
'Refresh Plex server URLs',
|
||||
'Backup Tautulli database',
|
||||
'Backup Tautulli config'
|
||||
]
|
||||
|
||||
DATE_TIME_FORMATS = [
|
||||
{
|
||||
'category': 'Year',
|
||||
'parameters': [
|
||||
{'value': 'YYYY', 'description': 'Numeric, four digits', 'example': '1999, 2003'},
|
||||
{'value': 'YY', 'description': 'Numeric, two digits', 'example': '99, 03'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Month',
|
||||
'parameters': [
|
||||
{'value': 'MMMM', 'description': 'Textual, full', 'example': 'January-December'},
|
||||
{'value': 'MMM', 'description': 'Textual, three letters', 'example': 'Jan-Dec'},
|
||||
{'value': 'MM', 'description': 'Numeric, with leading zeros', 'example': '42747'},
|
||||
{'value': 'M', 'description': 'Numeric, without leading zeros', 'example': '42747'},
|
||||
{'value': 'MM', 'description': 'Numeric, with leading zeros', 'example': '01-12'},
|
||||
{'value': 'M', 'description': 'Numeric, without leading zeros', 'example': '1-12'},
|
||||
{'value': 'Mo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 12th'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -163,14 +209,15 @@ DATE_TIME_FORMATS = [
|
||||
'parameters': [
|
||||
{'value': 'DDDD', 'description': 'Numeric, with leading zeros', 'example': '001-365'},
|
||||
{'value': 'DDD', 'description': 'Numeric, without leading zeros', 'example': '1-365'},
|
||||
{'value': 'DDDo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd, ... 365th'},
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Day of the Month',
|
||||
'parameters': [
|
||||
{'value': 'DD', 'description': 'Numeric, with leading zeros', 'example': '42766'},
|
||||
{'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '42766'},
|
||||
{'value': 'Do', 'description': 'Numeric, with suffix', 'example': 'E.g. 1st, 2nd ... 31st.'},
|
||||
{'value': 'DD', 'description': 'Numeric, with leading zeros', 'example': '01-31'},
|
||||
{'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '1-31'},
|
||||
{'value': 'Do', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 31st'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -178,7 +225,9 @@ DATE_TIME_FORMATS = [
|
||||
'parameters': [
|
||||
{'value': 'dddd', 'description': 'Textual, full', 'example': 'Sunday-Saturday'},
|
||||
{'value': 'ddd', 'description': 'Textual, three letters', 'example': 'Sun-Sat'},
|
||||
{'value': 'dd', 'description': 'Textual, two letters', 'example': 'Su-Sa'},
|
||||
{'value': 'd', 'description': 'Numeric', 'example': '0-6'},
|
||||
{'value': 'do', 'description': 'Numeric, with suffix', 'example': '0th, 1st ... 6th'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -186,8 +235,8 @@ DATE_TIME_FORMATS = [
|
||||
'parameters': [
|
||||
{'value': 'HH', 'description': '24-hour, with leading zeros', 'example': '00-23'},
|
||||
{'value': 'H', 'description': '24-hour, without leading zeros', 'example': '0-23'},
|
||||
{'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '42747'},
|
||||
{'value': 'h', 'description': '12-hour, without leading zeros', 'example': '42747'},
|
||||
{'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '01-12'},
|
||||
{'value': 'h', 'description': '12-hour, without leading zeros', 'example': '1-12'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -214,8 +263,8 @@ DATE_TIME_FORMATS = [
|
||||
{
|
||||
'category': 'Timezone',
|
||||
'parameters': [
|
||||
{'value': 'ZZ', 'description': 'UTC offset', 'example': 'E.g. +0100, -0700'},
|
||||
{'value': 'Z', 'description': 'UTC offset', 'example': 'E.g. +01:00, -07:00'},
|
||||
{'value': 'ZZ', 'description': 'UTC offset', 'example': '+0100, -0700'},
|
||||
{'value': 'Z', 'description': 'UTC offset', 'example': '+01:00, -07:00'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -224,7 +273,7 @@ DATE_TIME_FORMATS = [
|
||||
{'value': 'X', 'description': 'Unix timestamp', 'example': 'E.g. 1456887825'},
|
||||
]
|
||||
},
|
||||
]
|
||||
]
|
||||
|
||||
NOTIFICATION_PARAMETERS = [
|
||||
{
|
||||
@@ -306,7 +355,13 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Transcode Video Height', 'type': 'int', 'value': 'transcode_video_height', 'description': 'The video height of the transcoded stream.'},
|
||||
{'name': 'Transcode Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec', 'description': 'The audio codec of the transcoded stream.'},
|
||||
{'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels', 'description': 'The audio channels of the transcoded stream.'},
|
||||
{'name': 'Transcode Hardware', 'type': 'int', 'value': 'transcode_hardware', 'description': 'If hardware transcoding is used.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Requested', 'type': 'int', 'value': 'transcode_hw_requested', 'description': 'If hardware decoding/encoding was requested.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Decoding', 'type': 'int', 'value': 'transcode_hw_decoding', 'description': 'If hardware decoding is used.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Decoding Codec', 'type': 'str', 'value': 'transcode_hw_decode', 'description': 'The hardware decoding codec.'},
|
||||
{'name': 'Transcode HW Decoding Title', 'type': 'str', 'value': 'transcode_hw_decode_title', 'description': 'The hardware decoding codec title.'},
|
||||
{'name': 'Transcode HW Encoding', 'type': 'int', 'value': 'transcode_hw_encoding', 'description': 'If hardware encoding is used.', 'example': '0 or 1'},
|
||||
{'name': 'Transcode HW Encoding Codec', 'type': 'str', 'value': 'transcode_hw_encode', 'description': 'The hardware encoding codec.'},
|
||||
{'name': 'Transcode HW Encoding Title', 'type': 'str', 'value': 'transcode_hw_encode_title', 'description': 'The hardware encoding codec title.'},
|
||||
{'name': 'Session Key', 'type': 'str', 'value': 'session_key', 'description': 'The unique identifier for the session.'},
|
||||
{'name': 'Transcode Key', 'type': 'str', 'value': 'transcode_key', 'description': 'The unique identifier for the transcode session.'},
|
||||
{'name': 'Session ID', 'type': 'str', 'value': 'session_id', 'description': 'The unique identifier for the stream.'},
|
||||
@@ -430,4 +485,4 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Plexpy Update Changelog', 'type': 'int', 'value': 'plexpy_update_changelog', 'description': 'The changelog for the available update.'},
|
||||
]
|
||||
},
|
||||
]
|
||||
]
|
||||
|
@@ -289,6 +289,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'LOG_BLACKLIST': (int, 'General', 1),
|
||||
'LOG_DIR': (str, 'General', ''),
|
||||
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
||||
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
|
||||
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||
|
@@ -882,6 +882,7 @@ class DataFactory(object):
|
||||
'stream_video_framerate, ' \
|
||||
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
||||
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
||||
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
||||
'session_history_metadata.media_type, title, grandparent_title ' \
|
||||
'FROM session_history_media_info ' \
|
||||
'JOIN session_history ON session_history_media_info.id = session_history.id ' \
|
||||
@@ -899,6 +900,7 @@ class DataFactory(object):
|
||||
'stream_video_framerate, ' \
|
||||
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
||||
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
||||
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
||||
'media_type, title, grandparent_title ' \
|
||||
'FROM sessions ' \
|
||||
'WHERE session_key = ? %s' % user_cond
|
||||
@@ -945,11 +947,15 @@ class DataFactory(object):
|
||||
'subtitles': item['subtitles'],
|
||||
'stream_subtitle_decision': item['stream_subtitle_decision'],
|
||||
'stream_subtitle_codec': item['stream_subtitle_codec'],
|
||||
'transcode_hw_decoding': item['transcode_hw_decoding'],
|
||||
'transcode_hw_encoding': item['transcode_hw_encoding'],
|
||||
'media_type': item['media_type'],
|
||||
'title': item['title'],
|
||||
'grandparent_title': item['grandparent_title']
|
||||
'grandparent_title': item['grandparent_title'],
|
||||
'current_session': 1 if session_key else 0
|
||||
}
|
||||
|
||||
stream_output = {k: v or '' for k, v in stream_output.iteritems()}
|
||||
return stream_output
|
||||
|
||||
def get_metadata_details(self, rating_key):
|
||||
|
@@ -149,17 +149,17 @@ class HTTPHandler(object):
|
||||
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (self.uri, response_status))
|
||||
return None
|
||||
|
||||
def _http_format_output(self, response_content, response_headers):
|
||||
def _http_format_output(self, response_content, response_headers):
|
||||
"""Formats the request response to the desired type"""
|
||||
try:
|
||||
if self.output_format == 'text':
|
||||
output = response_content.decode('utf-8', 'ignore')
|
||||
if self.output_format == 'dict':
|
||||
output = helpers.convert_xml_to_dict(response_content.decode('utf-8', 'ignore'))
|
||||
elif self.output_format == 'dict':
|
||||
output = helpers.convert_xml_to_dict(response_content)
|
||||
elif self.output_format == 'json':
|
||||
output = helpers.convert_xml_to_json(response_content.decode('utf-8', 'ignore'))
|
||||
output = helpers.convert_xml_to_json(response_content)
|
||||
elif self.output_format == 'xml':
|
||||
output = helpers.parse_xml(response_content.decode('utf-8', 'ignore'))
|
||||
output = helpers.parse_xml(response_content)
|
||||
else:
|
||||
output = response_content
|
||||
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
import arrow
|
||||
import bleach
|
||||
from collections import Counter
|
||||
from collections import Counter, defaultdict
|
||||
from itertools import groupby
|
||||
import json
|
||||
from operator import itemgetter
|
||||
@@ -131,19 +131,19 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||
if stream_data:
|
||||
|
||||
# Check if notifications enabled for user and library
|
||||
user_data = users.Users()
|
||||
user_details = user_data.get_details(user_id=stream_data['user_id'])
|
||||
# user_data = users.Users()
|
||||
# user_details = user_data.get_details(user_id=stream_data['user_id'])
|
||||
#
|
||||
# library_data = libraries.Libraries()
|
||||
# library_details = library_data.get_details(section_id=stream_data['section_id'])
|
||||
|
||||
library_data = libraries.Libraries()
|
||||
library_details = library_data.get_details(section_id=stream_data['section_id'])
|
||||
|
||||
if not user_details['do_notify']:
|
||||
logger.debug(u"Tautulli NotificationHandler :: Notifications for user '%s' are disabled." % user_details['username'])
|
||||
return False
|
||||
|
||||
elif not library_details['do_notify'] and notify_action not in ('on_concurrent', 'on_newdevice'):
|
||||
logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' are disabled." % library_details['section_name'])
|
||||
return False
|
||||
# if not user_details['do_notify']:
|
||||
# logger.debug(u"Tautulli NotificationHandler :: Notifications for user '%s' are disabled." % user_details['username'])
|
||||
# return False
|
||||
#
|
||||
# elif not library_details['do_notify'] and notify_action not in ('on_concurrent', 'on_newdevice'):
|
||||
# logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' are disabled." % library_details['section_name'])
|
||||
# return False
|
||||
|
||||
if notify_action == 'on_concurrent':
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
@@ -188,12 +188,12 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||
elif timeline_data:
|
||||
|
||||
# Check if notifications enabled for library
|
||||
library_data = libraries.Libraries()
|
||||
library_details = library_data.get_details(section_id=timeline_data['section_id'])
|
||||
|
||||
if not library_details['do_notify_created']:
|
||||
# logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' is disabled." % library_details['section_name'])
|
||||
return False
|
||||
# library_data = libraries.Libraries()
|
||||
# library_details = library_data.get_details(section_id=timeline_data['section_id'])
|
||||
#
|
||||
# if not library_details['do_notify_created']:
|
||||
# # logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' is disabled." % library_details['section_name'])
|
||||
# return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -206,19 +206,21 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||
|
||||
custom_conditions_logic = notifier_config['custom_conditions_logic']
|
||||
custom_conditions = json.loads(notifier_config['custom_conditions']) or []
|
||||
|
||||
if custom_conditions_logic:
|
||||
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s." % notifier_id)
|
||||
if custom_conditions_logic or any(c for c in custom_conditions if c['value']):
|
||||
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s."
|
||||
% notifier_id)
|
||||
|
||||
custom_conditions = json.loads(notifier_config['custom_conditions'])
|
||||
|
||||
try:
|
||||
# Parse and validate the custom conditions logic
|
||||
logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
|
||||
except ValueError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
|
||||
% (custom_conditions_logic, e))
|
||||
return False
|
||||
logic_groups = None
|
||||
if custom_conditions_logic:
|
||||
try:
|
||||
# Parse and validate the custom conditions logic
|
||||
logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
|
||||
except ValueError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
|
||||
% (custom_conditions_logic, e))
|
||||
return False
|
||||
|
||||
evaluated_conditions = [None] # Set condition {0} to None
|
||||
|
||||
@@ -227,10 +229,11 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
operator = condition['operator']
|
||||
values = condition['value']
|
||||
parameter_type = condition['type']
|
||||
parameter_value = parameters.get(parameter, "")
|
||||
|
||||
# Set blank conditions to None
|
||||
# Set blank conditions to True (skip)
|
||||
if not parameter or not operator or not values:
|
||||
evaluated_conditions.append(None)
|
||||
evaluated_conditions.append(True)
|
||||
continue
|
||||
|
||||
# Make sure the condition values is in a list
|
||||
@@ -248,25 +251,25 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
elif parameter_type == 'float':
|
||||
values = [float(v) for v in values]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s' to type '%s'."
|
||||
% (parameter, parameter_type))
|
||||
except ValueError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s', values '%s', to type '%s'."
|
||||
% (parameter, values, parameter_type))
|
||||
return False
|
||||
|
||||
# Cast the parameter value to the correct type
|
||||
try:
|
||||
if parameter_type == 'str':
|
||||
parameter_value = unicode(parameters[parameter]).lower()
|
||||
parameter_value = unicode(parameter_value).lower()
|
||||
|
||||
elif parameter_type == 'int':
|
||||
parameter_value = int(parameters[parameter])
|
||||
parameter_value = int(parameter_value)
|
||||
|
||||
elif parameter_type == 'float':
|
||||
parameter_value = float(parameters[parameter])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s' to type '%s'."
|
||||
% (parameter, parameter_type))
|
||||
parameter_value = float(parameter_value)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s', value '%s', to type '%s'."
|
||||
% (parameter, parameter_value, parameter_type))
|
||||
return False
|
||||
|
||||
# Check each condition
|
||||
@@ -298,12 +301,15 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
logger.warn(u"Tautulli NotificationHandler :: Invalid condition operator '%s'." % operator)
|
||||
evaluated_conditions.append(None)
|
||||
|
||||
# Format and evaluate the logic string
|
||||
try:
|
||||
evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
|
||||
return False
|
||||
if logic_groups:
|
||||
# Format and evaluate the logic string
|
||||
try:
|
||||
evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
|
||||
return False
|
||||
else:
|
||||
evaluated_logic = all(evaluated_conditions[1:])
|
||||
|
||||
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
|
||||
return evaluated_logic
|
||||
@@ -326,7 +332,7 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
||||
if not notifier_config:
|
||||
return
|
||||
|
||||
if notify_action == 'test':
|
||||
if notify_action in ('test', 'api'):
|
||||
subject = kwargs.pop('subject', 'Tautulli')
|
||||
body = kwargs.pop('body', 'Test Notification')
|
||||
script_args = kwargs.pop('script_args', [])
|
||||
@@ -344,8 +350,8 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
||||
|
||||
# Set the notification state in the db
|
||||
notification_id = set_notify_state(session=stream_data or timeline_data,
|
||||
notify_action=notify_action,
|
||||
notifier=notifier_config,
|
||||
notify_action=notify_action,
|
||||
subject=subject,
|
||||
body=body,
|
||||
script_args=script_args)
|
||||
@@ -384,9 +390,9 @@ def get_notify_state(session):
|
||||
return notify_states
|
||||
|
||||
|
||||
def set_notify_state(notify_action, notifier, subject, body, script_args, session=None):
|
||||
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
||||
|
||||
if notify_action and notifier:
|
||||
if notifier and notify_action:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
session = session or {}
|
||||
@@ -449,17 +455,20 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
elif timeline:
|
||||
rating_key = timeline['rating_key']
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
|
||||
notify_params = defaultdict(str)
|
||||
if session:
|
||||
# Reload json from raw stream info
|
||||
if session.get('raw_stream_info'):
|
||||
session.update(json.loads(session['raw_stream_info']))
|
||||
notify_params.update(session)
|
||||
|
||||
if not metadata:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
|
||||
return None
|
||||
if timeline:
|
||||
notify_params.update(timeline)
|
||||
|
||||
## TODO: Check list of media info items, currently only grabs first item
|
||||
media_info = media_part_info = {}
|
||||
if 'media_info' in metadata and len(metadata['media_info']) > 0:
|
||||
media_info = metadata['media_info'][0]
|
||||
if 'media_info' in notify_params and len(notify_params['media_info']) > 0:
|
||||
media_info = notify_params['media_info'][0]
|
||||
if 'parts' in media_info and len(media_info['parts']) > 0:
|
||||
media_part_info = media_info.pop('parts')[0]
|
||||
|
||||
@@ -476,11 +485,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
media_part_info.update(stream)
|
||||
stream_subtitle = True
|
||||
|
||||
notify_params.update(media_info)
|
||||
notify_params.update(media_part_info)
|
||||
|
||||
child_metadata = grandchild_metadata = []
|
||||
for key in kwargs.pop('child_keys', []):
|
||||
child_metadata.append(pms_connect.get_metadata_details(rating_key=key))
|
||||
child_metadata.append(pmsconnect.PmsConnect().get_metadata_details(rating_key=key))
|
||||
for key in kwargs.pop('grandchild_keys', []):
|
||||
grandchild_metadata.append(pms_connect.get_metadata_details(rating_key=key))
|
||||
grandchild_metadata.append(pmsconnect.PmsConnect().get_metadata_details(rating_key=key))
|
||||
|
||||
# Session values
|
||||
session = session or {}
|
||||
@@ -507,102 +519,102 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
stream_duration = 0
|
||||
|
||||
view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 0))
|
||||
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
|
||||
duration = helpers.convert_milliseconds_to_minutes(notify_params['duration'])
|
||||
remaining_duration = duration - view_offset
|
||||
|
||||
# Build Plex URL
|
||||
metadata['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
rating_key=rating_key)
|
||||
|
||||
# Get media IDs from guid and build URLs
|
||||
if 'imdb://' in metadata['guid']:
|
||||
metadata['imdb_id'] = metadata['guid'].split('imdb://')[1].split('?')[0]
|
||||
metadata['imdb_url'] = 'https://www.imdb.com/title/' + metadata['imdb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/imdb/' + metadata['imdb_id']
|
||||
if 'imdb://' in notify_params['guid']:
|
||||
notify_params['imdb_id'] = notify_params['guid'].split('imdb://')[1].split('?')[0]
|
||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + notify_params['imdb_id']
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
||||
|
||||
if 'thetvdb://' in metadata['guid']:
|
||||
metadata['thetvdb_id'] = metadata['guid'].split('thetvdb://')[1].split('/')[0]
|
||||
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show'
|
||||
if 'thetvdb://' in notify_params['guid']:
|
||||
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdb://')[1].split('/')[0]
|
||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
||||
|
||||
elif 'thetvdbdvdorder://' in metadata['guid']:
|
||||
metadata['thetvdb_id'] = metadata['guid'].split('thetvdbdvdorder://')[1].split('/')[0]
|
||||
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show'
|
||||
elif 'thetvdbdvdorder://' in notify_params['guid']:
|
||||
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0]
|
||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
|
||||
|
||||
if 'themoviedb://' in metadata['guid']:
|
||||
if metadata['media_type'] == 'movie':
|
||||
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('?')[0]
|
||||
metadata['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + metadata['themoviedb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=movie'
|
||||
if 'themoviedb://' in notify_params['guid']:
|
||||
if notify_params['media_type'] == 'movie':
|
||||
notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('?')[0]
|
||||
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id']
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'
|
||||
|
||||
elif metadata['media_type'] in ('show', 'season', 'episode'):
|
||||
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('/')[0]
|
||||
metadata['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + metadata['themoviedb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=show'
|
||||
elif notify_params['media_type'] in ('show', 'season', 'episode'):
|
||||
notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('/')[0]
|
||||
notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
|
||||
|
||||
if 'lastfm://' in metadata['guid']:
|
||||
metadata['lastfm_id'] = metadata['guid'].split('lastfm://')[1].rsplit('/', 1)[0]
|
||||
metadata['lastfm_url'] = 'https://www.last.fm/music/' + metadata['lastfm_id']
|
||||
if 'lastfm://' in notify_params['guid']:
|
||||
notify_params['lastfm_id'] = notify_params['guid'].split('lastfm://')[1].rsplit('/', 1)[0]
|
||||
notify_params['lastfm_url'] = 'https://www.last.fm/music/' + notify_params['lastfm_id']
|
||||
|
||||
# Get TheMovieDB info
|
||||
if plexpy.CONFIG.THEMOVIEDB_LOOKUP:
|
||||
if metadata.get('themoviedb_id'):
|
||||
if notify_params.get('themoviedb_id'):
|
||||
themoveidb_json = get_themoviedb_info(rating_key=rating_key,
|
||||
media_type=metadata['media_type'],
|
||||
themoviedb_id=metadata['themoviedb_id'])
|
||||
media_type=notify_params['media_type'],
|
||||
themoviedb_id=notify_params['themoviedb_id'])
|
||||
|
||||
if themoveidb_json.get('imdb_id'):
|
||||
metadata['imdb_id'] = themoveidb_json['imdb_id']
|
||||
metadata['imdb_url'] = 'https://www.imdb.com/title/' + themoveidb_json['imdb_id']
|
||||
notify_params['imdb_id'] = themoveidb_json['imdb_id']
|
||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + themoveidb_json['imdb_id']
|
||||
|
||||
elif metadata.get('thetvdb_id') or metadata.get('imdb_id'):
|
||||
elif notify_params.get('thetvdb_id') or notify_params.get('imdb_id'):
|
||||
themoviedb_info = lookup_themoviedb_by_id(rating_key=rating_key,
|
||||
thetvdb_id=metadata.get('thetvdb_id'),
|
||||
imdb_id=metadata.get('imdb_id'))
|
||||
metadata.update(themoviedb_info)
|
||||
thetvdb_id=notify_params.get('thetvdb_id'),
|
||||
imdb_id=notify_params.get('imdb_id'))
|
||||
notify_params.update(themoviedb_info)
|
||||
|
||||
# Get TVmaze info (for tv shows only)
|
||||
if plexpy.CONFIG.TVMAZE_LOOKUP:
|
||||
if metadata['media_type'] in ('show', 'season', 'episode') and (metadata.get('thetvdb_id') or metadata.get('imdb_id')):
|
||||
if notify_params['media_type'] in ('show', 'season', 'episode') and (notify_params.get('thetvdb_id') or notify_params.get('imdb_id')):
|
||||
tvmaze_info = lookup_tvmaze_by_id(rating_key=rating_key,
|
||||
thetvdb_id=metadata.get('thetvdb_id'),
|
||||
imdb_id=metadata.get('imdb_id'))
|
||||
metadata.update(tvmaze_info)
|
||||
thetvdb_id=notify_params.get('thetvdb_id'),
|
||||
imdb_id=notify_params.get('imdb_id'))
|
||||
notify_params.update(tvmaze_info)
|
||||
|
||||
if tvmaze_info.get('thetvdb_id'):
|
||||
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id'])
|
||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id'])
|
||||
if tvmaze_info.get('imdb_id'):
|
||||
metadata['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
|
||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
|
||||
|
||||
if metadata['media_type'] in ('movie', 'show', 'artist'):
|
||||
poster_thumb = metadata['thumb']
|
||||
poster_key = metadata['rating_key']
|
||||
poster_title = metadata['title']
|
||||
elif metadata['media_type'] in ('season', 'album'):
|
||||
poster_thumb = metadata['thumb'] or metadata['parent_thumb']
|
||||
poster_key = metadata['rating_key']
|
||||
poster_title = '%s - %s' % (metadata['parent_title'],
|
||||
metadata['title'])
|
||||
elif metadata['media_type'] in ('episode', 'track'):
|
||||
poster_thumb = metadata['parent_thumb'] or metadata['grandparent_thumb']
|
||||
poster_key = metadata['parent_rating_key']
|
||||
poster_title = '%s - %s' % (metadata['grandparent_title'],
|
||||
metadata['parent_title'])
|
||||
if notify_params['media_type'] in ('movie', 'show', 'artist'):
|
||||
poster_thumb = notify_params['thumb']
|
||||
poster_key = notify_params['rating_key']
|
||||
poster_title = notify_params['title']
|
||||
elif notify_params['media_type'] in ('season', 'album'):
|
||||
poster_thumb = notify_params['thumb'] or notify_params['parent_thumb']
|
||||
poster_key = notify_params['rating_key']
|
||||
poster_title = '%s - %s' % (notify_params['parent_title'],
|
||||
notify_params['title'])
|
||||
elif notify_params['media_type'] in ('episode', 'track'):
|
||||
poster_thumb = notify_params['parent_thumb'] or notify_params['grandparent_thumb']
|
||||
poster_key = notify_params['parent_rating_key']
|
||||
poster_title = '%s - %s' % (notify_params['grandparent_title'],
|
||||
notify_params['parent_title'])
|
||||
else:
|
||||
poster_thumb = ''
|
||||
|
||||
if plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS:
|
||||
poster_info = get_poster_info(poster_thumb=poster_thumb, poster_key=poster_key, poster_title=poster_title)
|
||||
metadata.update(poster_info)
|
||||
notify_params.update(poster_info)
|
||||
|
||||
if ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT)
|
||||
and metadata['media_type'] in ('show', 'artist')):
|
||||
show_name = metadata['title']
|
||||
and notify_params['media_type'] in ('show', 'artist')):
|
||||
show_name = notify_params['title']
|
||||
episode_name = ''
|
||||
artist_name = metadata['title']
|
||||
artist_name = notify_params['title']
|
||||
album_name = ''
|
||||
track_name = ''
|
||||
|
||||
@@ -614,14 +626,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
track_num, track_num00 = '', ''
|
||||
|
||||
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
|
||||
and metadata['media_type'] in ('season', 'album')):
|
||||
show_name = metadata['parent_title']
|
||||
and notify_params['media_type'] in ('season', 'album')):
|
||||
show_name = notify_params['parent_title']
|
||||
episode_name = ''
|
||||
artist_name = metadata['parent_title']
|
||||
album_name = metadata['title']
|
||||
artist_name = notify_params['parent_title']
|
||||
album_name = notify_params['title']
|
||||
track_name = ''
|
||||
season_num = metadata['media_index'].zfill(1)
|
||||
season_num00 = metadata['media_index'].zfill(2)
|
||||
season_num = str(notify_params['media_index']).zfill(1)
|
||||
season_num00 = str(notify_params['media_index']).zfill(2)
|
||||
|
||||
num, num00 = format_group_index([helpers.cast_to_int(d['media_index'])
|
||||
for d in child_metadata if d['parent_rating_key'] == rating_key])
|
||||
@@ -629,192 +641,196 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
track_num, track_num00 = num, num00
|
||||
|
||||
else:
|
||||
show_name = metadata['grandparent_title']
|
||||
episode_name = metadata['title']
|
||||
artist_name = metadata['grandparent_title']
|
||||
album_name = metadata['parent_title']
|
||||
track_name = metadata['title']
|
||||
season_num = metadata['parent_media_index'].zfill(1)
|
||||
season_num00 = metadata['parent_media_index'].zfill(2)
|
||||
episode_num = metadata['media_index'].zfill(1)
|
||||
episode_num00 = metadata['media_index'].zfill(2)
|
||||
track_num = metadata['media_index'].zfill(1)
|
||||
track_num00 = metadata['media_index'].zfill(2)
|
||||
show_name = notify_params['grandparent_title']
|
||||
episode_name = notify_params['title']
|
||||
artist_name = notify_params['grandparent_title']
|
||||
album_name = notify_params['parent_title']
|
||||
track_name = notify_params['title']
|
||||
season_num = str(notify_params['parent_media_index']).zfill(1)
|
||||
season_num00 = str(notify_params['parent_media_index']).zfill(2)
|
||||
episode_num = str(notify_params['media_index']).zfill(1)
|
||||
episode_num00 = str(notify_params['media_index']).zfill(2)
|
||||
track_num = str(notify_params['media_index']).zfill(1)
|
||||
track_num00 = str(notify_params['media_index']).zfill(2)
|
||||
|
||||
available_params = {# Global paramaters
|
||||
'plexpy_version': common.VERSION_NUMBER,
|
||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
||||
'server_name': server_name,
|
||||
'server_uptime': server_uptime,
|
||||
'server_version': server_times.get('version',''),
|
||||
'action': notify_action.split('on_')[-1],
|
||||
'datestamp': arrow.now().format(date_format),
|
||||
'timestamp': arrow.now().format(time_format),
|
||||
# Stream parameters
|
||||
'streams': stream_count,
|
||||
'user_streams': user_stream_count,
|
||||
'user': session.get('friendly_name',''),
|
||||
'username': session.get('user',''),
|
||||
'device': session.get('device',''),
|
||||
'platform': session.get('platform',''),
|
||||
'product': session.get('product',''),
|
||||
'player': session.get('player',''),
|
||||
'ip_address': session.get('ip_address','N/A'),
|
||||
'stream_duration': stream_duration,
|
||||
'stream_time': arrow.get(stream_duration * 60).format(duration_format),
|
||||
'remaining_duration': remaining_duration,
|
||||
'remaining_time': arrow.get(remaining_duration * 60).format(duration_format),
|
||||
'progress_duration': view_offset,
|
||||
'progress_time': arrow.get(view_offset * 60).format(duration_format),
|
||||
'progress_percent': helpers.get_percent(view_offset, duration),
|
||||
'transcode_decision': transcode_decision,
|
||||
'video_decision': session.get('video_decision',''),
|
||||
'audio_decision': session.get('audio_decision',''),
|
||||
'subtitle_decision': session.get('subtitle_decision',''),
|
||||
'quality_profile': session.get('quality_profile',''),
|
||||
'optimized_version': session.get('optimized_version',''),
|
||||
'optimized_version_profile': session.get('optimized_version_profile',''),
|
||||
'stream_local': session.get('local', ''),
|
||||
'stream_location': session.get('location', ''),
|
||||
'stream_bandwidth': session.get('bandwidth', ''),
|
||||
'stream_container': session.get('stream_container', ''),
|
||||
'stream_bitrate': session.get('stream_bitrate', ''),
|
||||
'stream_aspect_ratio': session.get('stream_aspect_ratio', ''),
|
||||
'stream_video_codec': session.get('stream_video_codec', ''),
|
||||
'stream_video_codec_level': session.get('stream_video_codec_level', ''),
|
||||
'stream_video_bitrate': session.get('stream_video_bitrate', ''),
|
||||
'stream_video_bit_depth': session.get('stream_video_bit_depth', ''),
|
||||
'stream_video_framerate': session.get('stream_video_framerate', ''),
|
||||
'stream_video_ref_frames': session.get('stream_video_ref_frames', ''),
|
||||
'stream_video_resolution': session.get('stream_video_resolution', ''),
|
||||
'stream_video_height': session.get('stream_video_height', ''),
|
||||
'stream_video_width': session.get('stream_video_width', ''),
|
||||
'stream_video_language': session.get('stream_video_language', ''),
|
||||
'stream_video_language_code': session.get('stream_video_language_code', ''),
|
||||
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
|
||||
'stream_audio_bitrate_mode': session.get('stream_audio_bitrate_mode', ''),
|
||||
'stream_audio_codec': session.get('stream_audio_codec', ''),
|
||||
'stream_audio_channels': session.get('stream_audio_channels', ''),
|
||||
'stream_audio_channel_layout': session.get('stream_audio_channel_layout', ''),
|
||||
'stream_audio_sample_rate': session.get('stream_audio_sample_rate', ''),
|
||||
'stream_audio_language': session.get('stream_audio_language', ''),
|
||||
'stream_audio_language_code': session.get('stream_audio_language_code', ''),
|
||||
'stream_subtitle_codec': session.get('stream_subtitle_codec', ''),
|
||||
'stream_subtitle_container': session.get('stream_subtitle_container', ''),
|
||||
'stream_subtitle_format': session.get('stream_subtitle_format', ''),
|
||||
'stream_subtitle_forced': session.get('stream_subtitle_forced', ''),
|
||||
'stream_subtitle_language': session.get('stream_subtitle_language', ''),
|
||||
'stream_subtitle_language_code': session.get('stream_subtitle_language_code', ''),
|
||||
'stream_subtitle_location': session.get('stream_subtitle_location', ''),
|
||||
'transcode_container': session.get('transcode_container',''),
|
||||
'transcode_video_codec': session.get('transcode_video_codec',''),
|
||||
'transcode_video_width': session.get('transcode_width',''),
|
||||
'transcode_video_height': session.get('transcode_height',''),
|
||||
'transcode_audio_codec': session.get('transcode_audio_codec',''),
|
||||
'transcode_audio_channels': session.get('transcode_audio_channels',''),
|
||||
'transcode_hw_requested': session.get('transcode_hw_requested',''),
|
||||
'transcode_hw_decode': session.get('transcode_hw_decode',''),
|
||||
'transcode_hw_decode_title': session.get('transcode_hw_decode_title',''),
|
||||
'transcode_hw_encode': session.get('transcode_hw_encode',''),
|
||||
'transcode_hw_encode_title': session.get('transcode_hw_encode_title',''),
|
||||
'transcode_hw_full_pipeline': session.get('transcode_hw_full_pipeline',''),
|
||||
'session_key': session.get('session_key',''),
|
||||
'transcode_key': session.get('transcode_key',''),
|
||||
'session_id': session.get('session_id',''),
|
||||
'user_id': session.get('user_id',''),
|
||||
'machine_id': session.get('machine_id',''),
|
||||
# Source metadata parameters
|
||||
'media_type': metadata['media_type'],
|
||||
'title': metadata['full_title'],
|
||||
'library_name': metadata['library_name'],
|
||||
'show_name': show_name,
|
||||
'episode_name': episode_name,
|
||||
'artist_name': artist_name,
|
||||
'album_name': album_name,
|
||||
'track_name': track_name,
|
||||
'season_num': season_num,
|
||||
'season_num00': season_num00,
|
||||
'episode_num': episode_num,
|
||||
'episode_num00': episode_num00,
|
||||
'track_num': track_num,
|
||||
'track_num00': track_num00,
|
||||
'year': metadata['year'],
|
||||
'release_date': arrow.get(metadata['originally_available_at']).format(date_format)
|
||||
if metadata['originally_available_at'] else '',
|
||||
'air_date': arrow.get(metadata['originally_available_at']).format(date_format)
|
||||
if metadata['originally_available_at'] else '',
|
||||
'added_date': arrow.get(metadata['added_at']).format(date_format)
|
||||
if metadata['added_at'] else '',
|
||||
'updated_date': arrow.get(metadata['updated_at']).format(date_format)
|
||||
if metadata['updated_at'] else '',
|
||||
'last_viewed_date': arrow.get(metadata['last_viewed_at']).format(date_format)
|
||||
if metadata['last_viewed_at'] else '',
|
||||
'studio': metadata['studio'],
|
||||
'content_rating': metadata['content_rating'],
|
||||
'directors': ', '.join(metadata['directors']),
|
||||
'writers': ', '.join(metadata['writers']),
|
||||
'actors': ', '.join(metadata['actors']),
|
||||
'genres': ', '.join(metadata['genres']),
|
||||
'summary': metadata['summary'],
|
||||
'tagline': metadata['tagline'],
|
||||
'rating': metadata['rating'],
|
||||
'audience_rating': helpers.get_percent(metadata['audience_rating'], 10) or '',
|
||||
'duration': duration,
|
||||
'poster_title': metadata.get('poster_title',''),
|
||||
'poster_url': metadata.get('poster_url',''),
|
||||
'plex_url': metadata.get('plex_url',''),
|
||||
'imdb_id': metadata.get('imdb_id',''),
|
||||
'imdb_url': metadata.get('imdb_url',''),
|
||||
'thetvdb_id': metadata.get('thetvdb_id',''),
|
||||
'thetvdb_url': metadata.get('thetvdb_url',''),
|
||||
'themoviedb_id': metadata.get('themoviedb_id',''),
|
||||
'themoviedb_url': metadata.get('themoviedb_url',''),
|
||||
'tvmaze_id': metadata.get('tvmaze_id',''),
|
||||
'tvmaze_url': metadata.get('tvmaze_url',''),
|
||||
'lastfm_url': metadata.get('lastfm_url',''),
|
||||
'trakt_url': metadata.get('trakt_url',''),
|
||||
'container': session.get('container', media_info.get('container','')),
|
||||
'bitrate': session.get('bitrate', media_info.get('bitrate','')),
|
||||
'aspect_ratio': session.get('aspect_ratio', media_info.get('aspect_ratio','')),
|
||||
'video_codec': session.get('video_codec', media_part_info.get('video_codec','')),
|
||||
'video_codec_level': session.get('video_codec_level', media_part_info.get('video_codec_level','')),
|
||||
'video_bitrate': session.get('video_bitrate', media_part_info.get('video_bitrate','')),
|
||||
'video_bit_depth': session.get('video_bit_depth', media_part_info.get('video_bit_depth','')),
|
||||
'video_framerate': session.get('video_framerate', media_info.get('video_framerate','')),
|
||||
'video_ref_frames': session.get('video_ref_frames', media_part_info.get('video_ref_frames','')),
|
||||
'video_resolution': session.get('video_resolution', media_info.get('video_resolution','')),
|
||||
'video_height': session.get('height', media_info.get('height','')),
|
||||
'video_width': session.get('width', media_info.get('width','')),
|
||||
'video_language': session.get('video_language', media_part_info.get('video_language','')),
|
||||
'video_language_code': session.get('video_language_code', media_part_info.get('video_language_code','')),
|
||||
'audio_bitrate': session.get('audio_bitrate', media_part_info.get('audio_bitrate','')),
|
||||
'audio_bitrate_mode': session.get('audio_bitrate_mode', media_part_info.get('audio_bitrate_mode','')),
|
||||
'audio_codec': session.get('audio_codec', media_part_info.get('audio_codec','')),
|
||||
'audio_channels': session.get('audio_channels', media_part_info.get('audio_channels','')),
|
||||
'audio_channel_layout': session.get('audio_channel_layout', media_part_info.get('audio_channel_layout','')),
|
||||
'audio_sample_rate': session.get('audio_sample_rate', media_part_info.get('audio_sample_rate','')),
|
||||
'audio_language': session.get('audio_language', media_part_info.get('audio_language','')),
|
||||
'audio_language_code': session.get('audio_language_code', media_part_info.get('audio_language_code','')),
|
||||
'subtitle_codec': session.get('subtitle_codec', media_part_info.get('subtitle_codec','')),
|
||||
'subtitle_container': session.get('subtitle_container', media_part_info.get('subtitle_container','')),
|
||||
'subtitle_format': session.get('subtitle_format', media_part_info.get('subtitle_format','')),
|
||||
'subtitle_forced': session.get('subtitle_forced', media_part_info.get('subtitle_forced','')),
|
||||
'subtitle_location': session.get('subtitle_location', media_part_info.get('subtitle_location','')),
|
||||
'subtitle_language': session.get('subtitle_language', media_part_info.get('subtitle_language','')),
|
||||
'subtitle_language_code': session.get('subtitle_language_code', media_part_info.get('subtitle_language_code','')),
|
||||
'file': media_part_info.get('file',''),
|
||||
'file_size': helpers.humanFileSize(media_part_info.get('file_size','')),
|
||||
'indexes': media_part_info.get('indexes',''),
|
||||
'section_id': metadata['section_id'],
|
||||
'rating_key': metadata['rating_key'],
|
||||
'parent_rating_key': metadata['parent_rating_key'],
|
||||
'grandparent_rating_key': metadata['grandparent_rating_key'],
|
||||
'thumb': metadata['thumb'],
|
||||
'parent_thumb': metadata['parent_thumb'],
|
||||
'grandparent_thumb': metadata['grandparent_thumb'],
|
||||
'poster_thumb': poster_thumb
|
||||
}
|
||||
available_params = {
|
||||
# Global paramaters
|
||||
'plexpy_version': common.VERSION_NUMBER,
|
||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
||||
'server_name': server_name,
|
||||
'server_uptime': server_uptime,
|
||||
'server_version': server_times.get('version', ''),
|
||||
'action': notify_action.lstrip('on_'),
|
||||
'datestamp': arrow.now().format(date_format),
|
||||
'timestamp': arrow.now().format(time_format),
|
||||
# Stream parameters
|
||||
'streams': stream_count,
|
||||
'user_streams': user_stream_count,
|
||||
'user': notify_params['friendly_name'],
|
||||
'username': notify_params['user'],
|
||||
'device': notify_params['device'],
|
||||
'platform': notify_params['platform'],
|
||||
'product': notify_params['product'],
|
||||
'player': notify_params['player'],
|
||||
'ip_address': notify_params.get('ip_address', 'N/A'),
|
||||
'stream_duration': stream_duration,
|
||||
'stream_time': arrow.get(stream_duration * 60).format(duration_format),
|
||||
'remaining_duration': remaining_duration,
|
||||
'remaining_time': arrow.get(remaining_duration * 60).format(duration_format),
|
||||
'progress_duration': view_offset,
|
||||
'progress_time': arrow.get(view_offset * 60).format(duration_format),
|
||||
'progress_percent': helpers.get_percent(view_offset, duration),
|
||||
'transcode_decision': transcode_decision,
|
||||
'video_decision': notify_params['video_decision'],
|
||||
'audio_decision': notify_params['audio_decision'],
|
||||
'subtitle_decision': notify_params['subtitle_decision'],
|
||||
'quality_profile': notify_params['quality_profile'],
|
||||
'optimized_version': notify_params['optimized_version'],
|
||||
'optimized_version_profile': notify_params['optimized_version_profile'],
|
||||
'synced_version': notify_params['synced_version'],
|
||||
'stream_local': notify_params['local'],
|
||||
'stream_location': notify_params['location'],
|
||||
'stream_bandwidth': notify_params['bandwidth'],
|
||||
'stream_container': notify_params['stream_container'],
|
||||
'stream_bitrate': notify_params['stream_bitrate'],
|
||||
'stream_aspect_ratio': notify_params['stream_aspect_ratio'],
|
||||
'stream_video_codec': notify_params['stream_video_codec'],
|
||||
'stream_video_codec_level': notify_params['stream_video_codec_level'],
|
||||
'stream_video_bitrate': notify_params['stream_video_bitrate'],
|
||||
'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
|
||||
'stream_video_framerate': notify_params['stream_video_framerate'],
|
||||
'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
|
||||
'stream_video_resolution': notify_params['stream_video_resolution'],
|
||||
'stream_video_height': notify_params['stream_video_height'],
|
||||
'stream_video_width': notify_params['stream_video_width'],
|
||||
'stream_video_language': notify_params['stream_video_language'],
|
||||
'stream_video_language_code': notify_params['stream_video_language_code'],
|
||||
'stream_audio_bitrate': notify_params['stream_audio_bitrate'],
|
||||
'stream_audio_bitrate_mode': notify_params['stream_audio_bitrate_mode'],
|
||||
'stream_audio_codec': notify_params['stream_audio_codec'],
|
||||
'stream_audio_channels': notify_params['stream_audio_channels'],
|
||||
'stream_audio_channel_layout': notify_params['stream_audio_channel_layout'],
|
||||
'stream_audio_sample_rate': notify_params['stream_audio_sample_rate'],
|
||||
'stream_audio_language': notify_params['stream_audio_language'],
|
||||
'stream_audio_language_code': notify_params['stream_audio_language_code'],
|
||||
'stream_subtitle_codec': notify_params['stream_subtitle_codec'],
|
||||
'stream_subtitle_container': notify_params['stream_subtitle_container'],
|
||||
'stream_subtitle_format': notify_params['stream_subtitle_format'],
|
||||
'stream_subtitle_forced': notify_params['stream_subtitle_forced'],
|
||||
'stream_subtitle_language': notify_params['stream_subtitle_language'],
|
||||
'stream_subtitle_language_code': notify_params['stream_subtitle_language_code'],
|
||||
'stream_subtitle_location': notify_params['stream_subtitle_location'],
|
||||
'transcode_container': notify_params['transcode_container'],
|
||||
'transcode_video_codec': notify_params['transcode_video_codec'],
|
||||
'transcode_video_width': notify_params['transcode_width'],
|
||||
'transcode_video_height': notify_params['transcode_height'],
|
||||
'transcode_audio_codec': notify_params['transcode_audio_codec'],
|
||||
'transcode_audio_channels': notify_params['transcode_audio_channels'],
|
||||
'transcode_hw_requested': notify_params['transcode_hw_requested'],
|
||||
'transcode_hw_decoding': notify_params['transcode_hw_decoding'],
|
||||
'transcode_hw_decode_codec': notify_params['transcode_hw_decode'],
|
||||
'transcode_hw_decode_title': notify_params['transcode_hw_decode_title'],
|
||||
'transcode_hw_encoding': notify_params['transcode_hw_encoding'],
|
||||
'transcode_hw_encode_codec': notify_params['transcode_hw_encode'],
|
||||
'transcode_hw_encode_title': notify_params['transcode_hw_encode_title'],
|
||||
'transcode_hw_full_pipeline': notify_params['transcode_hw_full_pipeline'],
|
||||
'session_key': notify_params['session_key'],
|
||||
'transcode_key': notify_params['transcode_key'],
|
||||
'session_id': notify_params['session_id'],
|
||||
'user_id': notify_params['user_id'],
|
||||
'machine_id': notify_params['machine_id'],
|
||||
# Source metadata parameters
|
||||
'media_type': notify_params['media_type'],
|
||||
'title': notify_params['full_title'],
|
||||
'library_name': notify_params['library_name'],
|
||||
'show_name': show_name,
|
||||
'episode_name': episode_name,
|
||||
'artist_name': artist_name,
|
||||
'album_name': album_name,
|
||||
'track_name': track_name,
|
||||
'season_num': season_num,
|
||||
'season_num00': season_num00,
|
||||
'episode_num': episode_num,
|
||||
'episode_num00': episode_num00,
|
||||
'track_num': track_num,
|
||||
'track_num00': track_num00,
|
||||
'year': notify_params['year'],
|
||||
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
|
||||
if notify_params['originally_available_at'] else '',
|
||||
'air_date': arrow.get(notify_params['originally_available_at']).format(date_format)
|
||||
if notify_params['originally_available_at'] else '',
|
||||
'added_date': arrow.get(notify_params['added_at']).format(date_format)
|
||||
if notify_params['added_at'] else '',
|
||||
'updated_date': arrow.get(notify_params['updated_at']).format(date_format)
|
||||
if notify_params['updated_at'] else '',
|
||||
'last_viewed_date': arrow.get(notify_params['last_viewed_at']).format(date_format)
|
||||
if notify_params['last_viewed_at'] else '',
|
||||
'studio': notify_params['studio'],
|
||||
'content_rating': notify_params['content_rating'],
|
||||
'directors': ', '.join(notify_params['directors']),
|
||||
'writers': ', '.join(notify_params['writers']),
|
||||
'actors': ', '.join(notify_params['actors']),
|
||||
'genres': ', '.join(notify_params['genres']),
|
||||
'summary': notify_params['summary'],
|
||||
'tagline': notify_params['tagline'],
|
||||
'rating': notify_params['rating'],
|
||||
'audience_rating': helpers.get_percent(notify_params['audience_rating'], 10) or '',
|
||||
'duration': duration,
|
||||
'poster_title': notify_params['poster_title'],
|
||||
'poster_url': notify_params['poster_url'],
|
||||
'plex_url': notify_params['plex_url'],
|
||||
'imdb_id': notify_params['imdb_id'],
|
||||
'imdb_url': notify_params['imdb_url'],
|
||||
'thetvdb_id': notify_params['thetvdb_id'],
|
||||
'thetvdb_url': notify_params['thetvdb_url'],
|
||||
'themoviedb_id': notify_params['themoviedb_id'],
|
||||
'themoviedb_url': notify_params['themoviedb_url'],
|
||||
'tvmaze_id': notify_params['tvmaze_id'],
|
||||
'tvmaze_url': notify_params['tvmaze_url'],
|
||||
'lastfm_url': notify_params['lastfm_url'],
|
||||
'trakt_url': notify_params['trakt_url'],
|
||||
'container': notify_params['container'],
|
||||
'bitrate': notify_params['bitrate'],
|
||||
'aspect_ratio': notify_params['aspect_ratio'],
|
||||
'video_codec': notify_params['video_codec'],
|
||||
'video_codec_level': notify_params['video_codec_level'],
|
||||
'video_bitrate': notify_params['video_bitrate'],
|
||||
'video_bit_depth': notify_params['video_bit_depth'],
|
||||
'video_framerate': notify_params['video_framerate'],
|
||||
'video_ref_frames': notify_params['video_ref_frames'],
|
||||
'video_resolution': notify_params['video_resolution'],
|
||||
'video_height': notify_params['height'],
|
||||
'video_width': notify_params['width'],
|
||||
'video_language': notify_params['video_language'],
|
||||
'video_language_code': notify_params['video_language_code'],
|
||||
'audio_bitrate': notify_params['audio_bitrate'],
|
||||
'audio_bitrate_mode': notify_params['audio_bitrate_mode'],
|
||||
'audio_codec': notify_params['audio_codec'],
|
||||
'audio_channels': notify_params['audio_channels'],
|
||||
'audio_channel_layout': notify_params['audio_channel_layout'],
|
||||
'audio_sample_rate': notify_params['audio_sample_rate'],
|
||||
'audio_language': notify_params['audio_language'],
|
||||
'audio_language_code': notify_params['audio_language_code'],
|
||||
'subtitle_codec': notify_params['subtitle_codec'],
|
||||
'subtitle_container': notify_params['subtitle_container'],
|
||||
'subtitle_format': notify_params['subtitle_format'],
|
||||
'subtitle_forced': notify_params['subtitle_forced'],
|
||||
'subtitle_location': notify_params['subtitle_location'],
|
||||
'subtitle_language': notify_params['subtitle_language'],
|
||||
'subtitle_language_code': notify_params['subtitle_language_code'],
|
||||
'file': notify_params['file'],
|
||||
'file_size': helpers.humanFileSize(notify_params['file_size']),
|
||||
'indexes': notify_params['indexes'],
|
||||
'section_id': notify_params['section_id'],
|
||||
'rating_key': notify_params['rating_key'],
|
||||
'parent_rating_key': notify_params['parent_rating_key'],
|
||||
'grandparent_rating_key': notify_params['grandparent_rating_key'],
|
||||
'thumb': notify_params['thumb'],
|
||||
'parent_thumb': notify_params['parent_thumb'],
|
||||
'grandparent_thumb': notify_params['grandparent_thumb'],
|
||||
'poster_thumb': poster_thumb
|
||||
}
|
||||
|
||||
return available_params
|
||||
|
||||
@@ -831,8 +847,8 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
||||
plex_tv = plextv.PlexTV()
|
||||
server_times = plex_tv.get_server_times()
|
||||
|
||||
pms_download_info = kwargs.pop('pms_download_info', {})
|
||||
plexpy_download_info = kwargs.pop('plexpy_download_info', {})
|
||||
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
|
||||
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
|
||||
|
||||
if server_times:
|
||||
updated_at = server_times['updated_at']
|
||||
@@ -841,37 +857,38 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
|
||||
server_uptime = 'N/A'
|
||||
|
||||
available_params = {# Global paramaters
|
||||
'plexpy_version': common.VERSION_NUMBER,
|
||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
||||
'server_name': server_name,
|
||||
'server_uptime': server_uptime,
|
||||
'server_version': server_times.get('version',''),
|
||||
'action': notify_action.split('on_')[-1],
|
||||
'datestamp': arrow.now().format(date_format),
|
||||
'timestamp': arrow.now().format(time_format),
|
||||
# Plex Media Server update parameters
|
||||
'update_version': pms_download_info.get('version',''),
|
||||
'update_url': pms_download_info.get('download_url',''),
|
||||
'update_release_date': arrow.get(pms_download_info.get('release_date','')).format(date_format)
|
||||
if pms_download_info.get('release_date','') else '',
|
||||
'update_channel': 'Plex Pass' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public',
|
||||
'update_platform': pms_download_info.get('platform',''),
|
||||
'update_distro': pms_download_info.get('distro',''),
|
||||
'update_distro_build': pms_download_info.get('build',''),
|
||||
'update_requirements': pms_download_info.get('requirements',''),
|
||||
'update_extra_info': pms_download_info.get('extra_info',''),
|
||||
'update_changelog_added': pms_download_info.get('changelog_added',''),
|
||||
'update_changelog_fixed': pms_download_info.get('changelog_fixed',''),
|
||||
# Tautulli update parameters
|
||||
'plexpy_update_version': plexpy_download_info.get('tag_name', ''),
|
||||
'plexpy_update_tar': plexpy_download_info.get('tarball_url', ''),
|
||||
'plexpy_update_zip': plexpy_download_info.get('zipball_url', ''),
|
||||
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''),
|
||||
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''),
|
||||
'plexpy_update_changelog': plexpy_download_info.get('body', '')
|
||||
}
|
||||
available_params = {
|
||||
# Global paramaters
|
||||
'plexpy_version': common.VERSION_NUMBER,
|
||||
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
|
||||
'plexpy_commit': plexpy.CURRENT_VERSION,
|
||||
'server_name': server_name,
|
||||
'server_uptime': server_uptime,
|
||||
'server_version': server_times.get('version', ''),
|
||||
'action': notify_action.lstrip('on_'),
|
||||
'datestamp': arrow.now().format(date_format),
|
||||
'timestamp': arrow.now().format(time_format),
|
||||
# Plex Media Server update parameters
|
||||
'update_version': pms_download_info['version'],
|
||||
'update_url': pms_download_info['download_url'],
|
||||
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
|
||||
if pms_download_info['release_date'] else '',
|
||||
'update_channel': 'Beta' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public',
|
||||
'update_platform': pms_download_info['platform'],
|
||||
'update_distro': pms_download_info['distro'],
|
||||
'update_distro_build': pms_download_info['build'],
|
||||
'update_requirements': pms_download_info['requirements'],
|
||||
'update_extra_info': pms_download_info['extra_info'],
|
||||
'update_changelog_added': pms_download_info['changelog_added'],
|
||||
'update_changelog_fixed': pms_download_info['changelog_fixed'],
|
||||
# Tautulli update parameters
|
||||
'plexpy_update_version': plexpy_download_info['tag_name'],
|
||||
'plexpy_update_tar': plexpy_download_info['tarball_url'],
|
||||
'plexpy_update_zip': plexpy_download_info['zipball_url'],
|
||||
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''),
|
||||
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''),
|
||||
'plexpy_update_changelog': plexpy_download_info['body']
|
||||
}
|
||||
|
||||
return available_params
|
||||
|
||||
@@ -930,7 +947,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||
try:
|
||||
script_args = [custom_formatter.format(unicode(arg), **parameters) for arg in subject.split()]
|
||||
except LookupError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in script argument. Using fallback." % e)
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
|
||||
script_args = []
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
|
||||
@@ -941,7 +958,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||
try:
|
||||
subject = custom_formatter.format(unicode(subject), **parameters)
|
||||
except LookupError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
||||
subject = unicode(default_subject).format(**parameters)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
||||
@@ -950,7 +967,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||
try:
|
||||
body = custom_formatter.format(unicode(body), **parameters)
|
||||
except LookupError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
|
||||
body = unicode(default_body).format(**parameters)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
||||
|
@@ -61,7 +61,6 @@ import mobile_app
|
||||
import pmsconnect
|
||||
import request
|
||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||
from plexpy.helpers import checked
|
||||
|
||||
|
||||
AGENT_IDS = {'growl': 0,
|
||||
|
@@ -13,6 +13,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib
|
||||
|
||||
import plexpy
|
||||
@@ -195,7 +198,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/hubs/metadata/' + rating_key + '/related'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -520,7 +522,7 @@ class PmsConnect(object):
|
||||
|
||||
return output
|
||||
|
||||
def get_metadata_details(self, rating_key='', sync_id=''):
|
||||
def get_metadata_details(self, rating_key='', sync_id='', cache_key=None):
|
||||
"""
|
||||
Return processed and validated metadata list for requested item.
|
||||
|
||||
@@ -528,19 +530,33 @@ class PmsConnect(object):
|
||||
|
||||
Output: array
|
||||
"""
|
||||
metadata = {}
|
||||
|
||||
if cache_key:
|
||||
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
||||
try:
|
||||
with open(in_file_path, 'r') as inFile:
|
||||
metadata = json.load(inFile)
|
||||
except IOError as e:
|
||||
pass
|
||||
|
||||
if metadata:
|
||||
_cache_time = metadata.pop('_cache_time', 0)
|
||||
# Return cached metadata if less than METADATA_CACHE_SECONDS ago
|
||||
if int(time.time()) - _cache_time <= plexpy.CONFIG.METADATA_CACHE_SECONDS:
|
||||
return metadata
|
||||
|
||||
if rating_key:
|
||||
metadata = self.get_metadata(str(rating_key), output_format='xml')
|
||||
metadata_xml = self.get_metadata(str(rating_key), output_format='xml')
|
||||
elif sync_id:
|
||||
metadata = self.get_sync_item(str(sync_id), output_format='xml')
|
||||
metadata_xml = self.get_sync_item(str(sync_id), output_format='xml')
|
||||
|
||||
try:
|
||||
xml_head = metadata.getElementsByTagName('MediaContainer')
|
||||
xml_head = metadata_xml.getElementsByTagName('MediaContainer')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata_details: %s." % e)
|
||||
return {}
|
||||
|
||||
metadata = {}
|
||||
|
||||
for a in xml_head:
|
||||
if a.getAttribute('size'):
|
||||
if a.getAttribute('size') != '1':
|
||||
@@ -1103,7 +1119,7 @@ class PmsConnect(object):
|
||||
'subtitle_codec': helpers.get_xml_attr(stream, 'codec'),
|
||||
'subtitle_container': helpers.get_xml_attr(stream, 'container'),
|
||||
'subtitle_format': helpers.get_xml_attr(stream, 'format'),
|
||||
'subtitle_forced': 1 if helpers.get_xml_attr(stream, 'forced') == '1' else 0,
|
||||
'subtitle_forced': int(helpers.get_xml_attr(stream, 'forced') == '1'),
|
||||
'subtitle_location': 'external' if helpers.get_xml_attr(stream, 'key') else 'embedded',
|
||||
'subtitle_language': helpers.get_xml_attr(stream, 'language'),
|
||||
'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode')
|
||||
@@ -1112,7 +1128,7 @@ class PmsConnect(object):
|
||||
parts.append({'id': helpers.get_xml_attr(part, 'id'),
|
||||
'file': helpers.get_xml_attr(part, 'file'),
|
||||
'file_size': helpers.get_xml_attr(part, 'size'),
|
||||
'indexes': 1 if helpers.get_xml_attr(part, 'indexes') == 'sd' else 0,
|
||||
'indexes': int(helpers.get_xml_attr(part, 'indexes') == 'sd'),
|
||||
'streams': streams
|
||||
})
|
||||
|
||||
@@ -1132,13 +1148,24 @@ class PmsConnect(object):
|
||||
'audio_channels': audio_channels,
|
||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||
'audio_profile': helpers.get_xml_attr(media, 'audioProfile'),
|
||||
'optimized_version': 1 if helpers.get_xml_attr(media, 'proxyType') == '42' else 0,
|
||||
'optimized_version': int(helpers.get_xml_attr(media, 'proxyType') == '42'),
|
||||
'parts': parts
|
||||
})
|
||||
|
||||
metadata['media_info'] = medias
|
||||
|
||||
if metadata:
|
||||
if cache_key:
|
||||
metadata['_cache_time'] = int(time.time())
|
||||
|
||||
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
||||
try:
|
||||
with open(out_file_path, 'w') as outFile:
|
||||
json.dump(metadata, outFile)
|
||||
except IOError as e:
|
||||
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
|
||||
% (cache_key, e))
|
||||
|
||||
return metadata
|
||||
else:
|
||||
return {}
|
||||
@@ -1300,6 +1327,7 @@ class PmsConnect(object):
|
||||
# Get the source media type
|
||||
media_type = helpers.get_xml_attr(session, 'type')
|
||||
rating_key = helpers.get_xml_attr(session, 'ratingKey')
|
||||
session_key = helpers.get_xml_attr(session, 'sessionKey')
|
||||
|
||||
# Get the user details
|
||||
user_info = session.getElementsByTagName('User')[0]
|
||||
@@ -1353,7 +1381,7 @@ class PmsConnect(object):
|
||||
transcode_speed = helpers.get_xml_attr(transcode_info, 'speed')
|
||||
|
||||
transcode_details = {'transcode_key': helpers.get_xml_attr(transcode_info, 'key'),
|
||||
'transcode_throttled': 1 if helpers.get_xml_attr(transcode_info, 'throttled') == '1' else 0,
|
||||
'transcode_throttled': int(helpers.get_xml_attr(transcode_info, 'throttled') == '1'),
|
||||
'transcode_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
|
||||
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
|
||||
'transcode_audio_channels': helpers.get_xml_attr(transcode_info, 'audioChannels'),
|
||||
@@ -1363,12 +1391,12 @@ class PmsConnect(object):
|
||||
'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), # Blank but keep backwards compatibility
|
||||
'transcode_container': helpers.get_xml_attr(transcode_info, 'container'),
|
||||
'transcode_protocol': helpers.get_xml_attr(transcode_info, 'protocol'),
|
||||
'transcode_hw_requested': 1 if helpers.get_xml_attr(transcode_info, 'transcodeHwRequested') == '1' else 0,
|
||||
'transcode_hw_requested': int(helpers.get_xml_attr(transcode_info, 'transcodeHwRequested') == '1'),
|
||||
'transcode_hw_decode': helpers.get_xml_attr(transcode_info, 'transcodeHwDecoding'),
|
||||
'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwDecodingTitle'),
|
||||
'transcode_hw_encode': helpers.get_xml_attr(transcode_info, 'transcodeHwEncoding'),
|
||||
'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwEncodingTitle'),
|
||||
'transcode_hw_full_pipeline': 1 if helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1' else 0,
|
||||
'transcode_hw_full_pipeline': int(helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1'),
|
||||
'audio_decision': helpers.get_xml_attr(transcode_info, 'audioDecision'),
|
||||
'video_decision': helpers.get_xml_attr(transcode_info, 'videoDecision'),
|
||||
'subtitle_decision': helpers.get_xml_attr(transcode_info, 'subtitleDecision'),
|
||||
@@ -1398,6 +1426,10 @@ class PmsConnect(object):
|
||||
'throttled': '0' # Keep for backwards compatibility
|
||||
}
|
||||
|
||||
# Check HW decoding/encoding
|
||||
transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS)
|
||||
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
|
||||
|
||||
# Generate a combined transcode decision value
|
||||
if transcode_details['video_decision'] == 'transcode' or transcode_details['audio_decision'] == 'transcode':
|
||||
transcode_decision = 'transcode'
|
||||
@@ -1490,7 +1522,7 @@ class PmsConnect(object):
|
||||
subtitle_details = {'stream_subtitle_codec': helpers.get_xml_attr(subtitle_stream_info, 'codec'),
|
||||
'stream_subtitle_container': helpers.get_xml_attr(subtitle_stream_info, 'container'),
|
||||
'stream_subtitle_format': helpers.get_xml_attr(subtitle_stream_info, 'format'),
|
||||
'stream_subtitle_forced': 1 if helpers.get_xml_attr(subtitle_stream_info, 'forced') == '1' else 0,
|
||||
'stream_subtitle_forced': int(helpers.get_xml_attr(subtitle_stream_info, 'forced') == '1'),
|
||||
'stream_subtitle_location': helpers.get_xml_attr(subtitle_stream_info, 'location'),
|
||||
'stream_subtitle_language': helpers.get_xml_attr(subtitle_stream_info, 'language'),
|
||||
'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info, 'languageCode'),
|
||||
@@ -1538,10 +1570,10 @@ class PmsConnect(object):
|
||||
'stream_duration': helpers.get_xml_attr(stream_media_info, 'duration') or helpers.get_xml_attr(session, 'duration'),
|
||||
'stream_container_decision': 'direct play' if sync_id else helpers.get_xml_attr(stream_media_parts_info, 'decision').replace('directplay', 'direct play'),
|
||||
'transcode_decision': transcode_decision,
|
||||
'optimized_version': 1 if helpers.get_xml_attr(stream_media_info, 'proxyType') == '42' else 0,
|
||||
'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'),
|
||||
'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
|
||||
'synced_version': 1 if sync_id else 0,
|
||||
'indexes': 1 if indexes == 'sd' else 0,
|
||||
'indexes': int(indexes == 'sd'),
|
||||
'bif_thumb': bif_thumb,
|
||||
'subtitles': 1 if subtitle_id and subtitle_selected else 0
|
||||
}
|
||||
@@ -1610,9 +1642,9 @@ class PmsConnect(object):
|
||||
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
|
||||
|
||||
if sync_id:
|
||||
metadata_details = self.get_metadata_details(sync_id=sync_id)
|
||||
metadata_details = self.get_metadata_details(sync_id=sync_id, cache_key=session_key)
|
||||
else:
|
||||
metadata_details = self.get_metadata_details(rating_key=rating_key)
|
||||
metadata_details = self.get_metadata_details(rating_key=rating_key, cache_key=session_key)
|
||||
|
||||
# Get the media info, fallback to first item if match id is not found
|
||||
source_medias = metadata_details.pop('media_info', [])
|
||||
@@ -1725,7 +1757,7 @@ class PmsConnect(object):
|
||||
optimized_version_profile = ''
|
||||
|
||||
# Entire session output (single dict for backwards compatibility)
|
||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||
session_output = {'session_key': session_key,
|
||||
'media_type': media_type,
|
||||
'view_offset': view_offset,
|
||||
'progress_percent': str(helpers.get_percent(view_offset, stream_details['stream_duration'])),
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.6-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.12-beta"
|
||||
|
@@ -178,6 +178,10 @@ def checkGithub(auto_update=False):
|
||||
url = 'https://api.github.com/repos/%s/plexpy/releases' % plexpy.CONFIG.GIT_USER
|
||||
releases = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
|
||||
|
||||
if releases is None:
|
||||
logger.warn('Could not get releases from GitHub.')
|
||||
return plexpy.LATEST_VERSION
|
||||
|
||||
if plexpy.CONFIG.GIT_BRANCH == 'master':
|
||||
release = next((r for r in releases if not r['prerelease']), releases[0])
|
||||
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
|
||||
|
@@ -96,7 +96,8 @@ def check_credentials(username, password, admin_login='0'):
|
||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
||||
return True, u'admin'
|
||||
elif username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||
return True, u'admin'
|
||||
elif not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS and user_login(username, password):
|
||||
return True, u'guest'
|
||||
|
@@ -3079,7 +3079,6 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi("notify")
|
||||
def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='', **kwargs):
|
||||
""" Send a notification using Tautulli.
|
||||
|
||||
@@ -4437,11 +4436,12 @@ class WebInterface(object):
|
||||
if session_key:
|
||||
return next((s for s in result['sessions'] if s['session_key'] == session_key), {})
|
||||
|
||||
|
||||
counts = {'stream_count_direct_play': 0,
|
||||
'stream_count_direct_stream': 0,
|
||||
'stream_count_transcode': 0,
|
||||
'total_bandwidth': 0}
|
||||
'total_bandwidth': 0,
|
||||
'lan_bandwidth': 0,
|
||||
'wan_bandwidth': 0}
|
||||
|
||||
for s in result['sessions']:
|
||||
if s['transcode_decision'] == 'transcode':
|
||||
@@ -4452,6 +4452,10 @@ class WebInterface(object):
|
||||
counts['stream_count_direct_play'] += 1
|
||||
|
||||
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
if s['location'] == 'lan':
|
||||
counts['lan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
elif s['location'] == 'wan':
|
||||
counts['wan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
|
||||
result.update(counts)
|
||||
|
||||
|
Reference in New Issue
Block a user