Compare commits

...

24 Commits

Author SHA1 Message Date
JonnyWong16
cf14fbc3f0 v2.0.11-beta 2018-01-05 21:50:35 -08:00
JonnyWong16
e471d5207d Remove experimental tag from calculate file sizes 2018-01-05 21:50:24 -08:00
JonnyWong16
5722a52082 Fix None values in stream data for pre v2 history 2018-01-05 21:37:54 -08:00
JonnyWong16
08c32e875e Fix login using hashed password 2018-01-05 21:01:32 -08:00
JonnyWong16
7d3ee3afb3 Fix recently added show title 2018-01-05 21:01:10 -08:00
JonnyWong16
def8600f5c Reload notify params from raw stream info 2018-01-05 14:22:20 -08:00
JonnyWong16
74a68f3c7d v2.0.10-beta 2018-01-04 19:55:13 -08:00
JonnyWong16
64c9247dd1 Remove library/user notification toggles
* Filter out notifications using custom conditions
2018-01-04 19:39:16 -08:00
JonnyWong16
1bfcd34247 Some formatting for common.py 2018-01-04 13:40:34 -08:00
JonnyWong16
19864e97e6 Fix media type in collection header 2018-01-04 13:40:34 -08:00
JonnyWong16
ec5c5e1420 Merge pull request #1195 from Tommatheussen/patch-1
Update date formats
2018-01-04 13:39:25 -08:00
Tom Matheussen
803f4e14ca Added some additional formats 2018-01-04 22:18:40 +01:00
Tom Matheussen
6cc254b80a Update Date Formats
Added correct Year date formats, replaced generic numeric values with actual examples
2018-01-04 21:32:59 +01:00
JonnyWong16
59593ab1aa Fix HW indicator on activity refresh 2018-01-03 20:29:52 -08:00
JonnyWong16
65a0a0eb7d v2.0.9-beta 2018-01-03 19:37:12 -08:00
JonnyWong16
f4206b401f Fix season/episode numbers zfill 2018-01-03 19:24:19 -08:00
JonnyWong16
99f8d24b3e Remove bottom padding on stats info 2018-01-03 16:35:22 -08:00
JonnyWong16
26b06e453d v2.0.8-beta 2018-01-03 16:08:21 -08:00
JonnyWong16
54ab646048 Don't line break product or player on activity cards 2018-01-03 16:02:22 -08:00
JonnyWong16
12c9aa3d6a Try caching metadata for sessions 2018-01-03 13:36:26 -08:00
JonnyWong16
1ae8544f2d Cleanup notification parameters 2018-01-03 11:36:49 -08:00
JonnyWong16
eae9e66c75 Updating missing notification parameters 2018-01-02 16:18:50 -08:00
JonnyWong16
ad041a1691 Attempt to fix HW transcoding indicator 2018-01-02 16:13:27 -08:00
JonnyWong16
1aee3b6c8f Add idna 2.6 2018-01-02 09:03:55 -08:00
32 changed files with 10549 additions and 602 deletions

View File

@@ -1,5 +1,40 @@
# Changelog # Changelog
## 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) ## v2.0.7-beta (2018-01-01)
* Monitoring: * Monitoring:

View File

@@ -844,6 +844,18 @@ a .users-poster-face:hover {
-webkit-flex-grow: 1; -webkit-flex-grow: 1;
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 { .dashboard-activity-info-item .sub-value .ip-container {
display: inline-flex; display: inline-flex;
} }
@@ -1261,7 +1273,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
.dashboard-stats-info { .dashboard-stats-info {
width: 100%; width: 100%;
font-size: 12px; font-size: 12px;
padding: 3px 0 5px 15px; padding: 3px 0 0 15px;
position: relative; position: relative;
} }
.dashboard-stats-info-list { .dashboard-stats-info-list {

View File

@@ -64,6 +64,7 @@ DOCUMENTATION :: END
from collections import defaultdict from collections import defaultdict
from urllib import quote from urllib import quote
from plexpy import helpers from plexpy import helpers
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
import plexpy import plexpy
%> %>
<% data = defaultdict(lambda: 'Unknown', **session) %> <% data = defaultdict(lambda: 'Unknown', **session) %>
@@ -134,15 +135,15 @@ DOCUMENTATION :: END
<ul class="list-unstyled dashboard-activity-info-list"> <ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Product</div> <div class="sub-heading">Product</div>
<div class="sub-value">${data['product']}</div> <div class="sub-value platform-right">${data['product']}</div>
</li> </li>
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Player</div> <div class="sub-heading">Player</div>
<div class="sub-value">${data['player']}</div> <div class="sub-value platform-right">${data['player']}</div>
</li> </li>
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Quality</div> <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': % if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown':
<% <%
br = helpers.cast_to_int(data['stream_bitrate']) or '' 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['media_type'] in ('movie', 'episode', 'clip'):
% if data.get('stream_video_decision') == 'transcode': % if data.get('stream_video_decision') == 'transcode':
<% <%
hw_d = hw_e = '' hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
if data['transcode_hw_requested'] == 1 and data['transcode_hw_full_pipeline'] == 0: hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
hw_d = ' (HW)'
elif data['transcode_hw_requested'] == 1 and data['transcode_hw_full_pipeline'] == 1:
hw_d = hw_e = ' (HW)'
%> %>
Transcode (${data['video_codec'].upper()}${hw_d} ${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} &rarr; ${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'])} &rarr; ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% elif data.get('stream_video_decision') == 'copy': % elif data.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: % 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 % endif
% elif data['media_type'] == 'photo': % elif data['media_type'] == 'photo':
Direct Play (${data['width']}x${data['height']}) Direct Play (${data['width']}x${data['height']})
@@ -237,11 +235,11 @@ DOCUMENTATION :: END
<div class="sub-heading">Audio</div> <div class="sub-heading">Audio</div>
<div class="sub-value" id="audio_decision-${sk}"> <div class="sub-value" id="audio_decision-${sk}">
% if data.get('stream_audio_decision') == 'transcode': % if data.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()} &rarr; ${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()} &rarr; ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% elif data.get('stream_audio_decision') == 'copy': % elif data.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: % 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 % endif
</div> </div>
</li> </li>
@@ -270,7 +268,7 @@ DOCUMENTATION :: END
<ul class="list-unstyled dashboard-activity-info-list"> <ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Location</div> <div class="sub-heading">Location</div>
<div class="sub-value"> <div class="sub-value time-right">
% if data['ip_address'] != 'N/A': % if data['ip_address'] != 'N/A':
${data['location'].upper()}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span> ${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']}"> <a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
@@ -290,7 +288,7 @@ DOCUMENTATION :: END
</li> </li>
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Bandwidth</div> <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']): % if data['media_type'] != 'photo' and helpers.cast_to_int(data['bandwidth']):
<% <%
bw = helpers.cast_to_int(data['bandwidth']) bw = helpers.cast_to_int(data['bandwidth'])

View File

@@ -47,24 +47,12 @@ DOCUMENTATION :: END
</div> </div>
<p class="help-block">Change the library's picture in Tautulli. To reset to default, leave this field empty and save.</p> <p class="help-block">Change the library's picture in Tautulli. To reset to default, leave this field empty and save.</p>
</div> </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"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history <input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history
</label> </label>
<p class="help-block">Uncheck this if you do not want to keep any history on this library's activity.</p> <p class="help-block">Uncheck this if you do not want to keep any history on this library's activity.</p>
</div> </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']: % if data['section_id']:
<div class="form-group"> <div class="form-group">
<button class="btn btn-danger" id="delete-all-history">Purge</button> <button class="btn btn-danger" id="delete-all-history">Purge</button>
@@ -85,15 +73,7 @@ DOCUMENTATION :: END
// Save library options // Save library options
$("#save_library").on('click', function () { $("#save_library").on('click', function () {
var custom_thumb = $("#custom_thumb_url").val(); var custom_thumb = $("#custom_thumb_url").val();
var do_notify = 0;
var do_notify_created = 0;
var keep_history = 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")) { if ($("#keep_history").is(":checked")) {
keep_history = 1; keep_history = 1;
} }
@@ -103,8 +83,6 @@ DOCUMENTATION :: END
data: { data: {
section_id: '${data["section_id"]}', section_id: '${data["section_id"]}',
custom_thumb: custom_thumb, custom_thumb: custom_thumb,
do_notify: do_notify,
do_notify_created: do_notify_created,
keep_history: keep_history keep_history: keep_history
}, },
cache: false, cache: false,

View File

@@ -56,12 +56,6 @@ DOCUMENTATION :: END
</div> </div>
<p class="help-block">Change the users profile picture in Tautulli. To reset to default, leave this field empty and save.</p> <p class="help-block">Change the users profile picture in Tautulli. To reset to default, leave this field empty and save.</p>
</div> </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"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history <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 () { $("#save_user").on('click', function () {
var friendly_name = $("input#friendly_name").val(); var friendly_name = $("input#friendly_name").val();
var custom_thumb = $("#custom_avatar_url").val(); var custom_thumb = $("#custom_avatar_url").val();
var do_notify = 0;
var keep_history = 0; var keep_history = 0;
var allow_guest = 0; var allow_guest = 0;
if ($("#do_notify").is(":checked")) {
do_notify = 1;
}
if ($("#keep_history").is(":checked")) { if ($("#keep_history").is(":checked")) {
keep_history = 1; keep_history = 1;
} }
@@ -114,7 +104,6 @@ DOCUMENTATION :: END
user_id: '${data["user_id"]}', user_id: '${data["user_id"]}',
friendly_name: friendly_name, friendly_name: friendly_name,
custom_thumb: custom_thumb, custom_thumb: custom_thumb,
do_notify: do_notify,
keep_history: keep_history, keep_history: keep_history,
allow_guest: allow_guest allow_guest: allow_guest
}, },

View File

@@ -325,25 +325,26 @@
} }
// Update play state icon // Update play state icon
var state_icon = '';
switch (s.state) { switch (s.state) {
case 'playing': case 'playing':
var state_icon = '<i class="fa fa-fw fa-play"></i>&nbsp;'; state_icon = '<i class="fa fa-fw fa-play"></i>&nbsp;';
break; break;
case 'paused': case 'paused':
var state_icon = '<i class="fa fa-fw fa-pause"></i>&nbsp;'; state_icon = '<i class="fa fa-fw fa-pause"></i>&nbsp;';
break; break;
case 'buffering': case 'buffering':
var state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;'; state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;';
break; break;
default: default:
var state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;'; state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;';
} }
$('#play-state-' + key).html(state_icon).attr('title', capitalizeFirstLetter(s.state)); $('#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. // Switching tracks can be under the same session key, so need to update the info.
if (s.media_type === 'track') { if (s.media_type === 'track') {
// Update if artist changed // 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)'); $('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&fallback=art&refresh=true)');
$('#metadata-grandparent_title-' + key) $('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key) .attr('href', 'info?rating_key=' + s.grandparent_rating_key)
@@ -351,7 +352,7 @@
.text(s.grandparent_title); .text(s.grandparent_title);
} }
// Update cover if album changed // 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).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-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
$('#poster-url-' + key) $('#poster-url-' + key)
@@ -363,7 +364,7 @@
.text(s.parent_title); .text(s.parent_title);
} }
// Update cover if track changed // 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) $('#metadata-title-' + key)
.attr('href', 'info?rating_key=' + s.rating_key) .attr('href', 'info?rating_key=' + s.rating_key)
.attr('title', s.title) .attr('title', s.title)
@@ -374,7 +375,7 @@
// Update the transcode state // Update the transcode state
var transcode_decision = ''; var transcode_decision = '';
if (s.transcode_decision === 'transcode') { 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; transcode_decision = 'Transcode' + throttled;
} else if (s.transcode_decision === 'copy') { } else if (s.transcode_decision === 'copy') {
transcode_decision = 'Direct Stream'; transcode_decision = 'Direct Stream';
@@ -392,36 +393,32 @@
$('#transcode_container-' + key).html(transcode_container); $('#transcode_container-' + key).html(transcode_container);
var video_decision = ''; 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()) { switch (s.video_resolution.toLowerCase()) {
case 'sd': case 'sd':
var v_res = 'SD'; v_res = 'SD';
break; break;
case '4k': case '4k':
var v_res = '4k'; v_res = '4k';
break; break;
default: default:
var v_res = s.video_resolution + 'p' v_res = s.video_resolution + 'p'
} }
var sv_res = '';
switch (s.stream_video_resolution.toLowerCase()) { switch (s.stream_video_resolution.toLowerCase()) {
case 'sd': case 'sd':
var sv_res = 'SD'; sv_res = 'SD';
break; break;
case '4k': case '4k':
var sv_res = '4k'; sv_res = '4k';
break; break;
default: default:
var sv_res = s.stream_video_resolution + 'p' sv_res = s.stream_video_resolution + 'p'
} }
if (s.stream_video_decision === 'transcode') { if (s.stream_video_decision === 'transcode') {
var hw_d = ''; var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
var hw_e = ''; var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
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)';
}
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' &rarr; ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')'; video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' &rarr; ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
} else if (s.stream_video_decision === 'copy') { } else if (s.stream_video_decision === 'copy') {
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')'; video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
@@ -434,7 +431,7 @@
$('#video_decision-' + key).html(video_decision); $('#video_decision-' + key).html(video_decision);
var audio_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 a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase(); var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
if (s.stream_audio_decision === 'transcode') { if (s.stream_audio_decision === 'transcode') {
@@ -456,13 +453,13 @@
} else if (s.stream_subtitle_decision === 'burn') { } else if (s.stream_subtitle_decision === 'burn') {
subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')'; subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')';
} else { } 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); $('#subtitle_decision-' + key).html(subtitle_decision);
// Update the stream quality profile and bandwidth // 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) || ''; var br = parseInt(s.stream_bitrate) || '';
if (br) { if (br) {
if (br > 1000) { if (br > 1000) {
@@ -478,9 +475,9 @@
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')'); $('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
$('#synced_quality_profile-' + key).html(s.synced_quality_profile); $('#synced_quality_profile-' + key).html(s.synced_quality_profile);
if (s.media_type != 'photo' && parseInt(s.bandwidth)) { if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
var bw = parseInt(s.bandwidth); var bw = parseInt(s.bandwidth);
if (bw != "Unknown") { if (bw !== "Unknown") {
if (bw > 1000) { if (bw > 1000) {
bw = (bw / 1000).toFixed(1) + ' Mbps'; bw = (bw / 1000).toFixed(1) + ' Mbps';
} else { } else {
@@ -492,17 +489,19 @@
// Update the stream progress times // Update the stream progress times
$('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format)); $('#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); var stream_view_offset = $('#stream-view-offset-' + key);
if ($('#stream-view-offset-' + key).data('last_view_offset') != s.view_offset) { stream_view_offset.data('state', s.state);
$('#stream-view-offset-' + key).data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); 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 // Update the progress bars, percent - 3 because of 3px padding-right
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%') $('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%'); .attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
$('#progress-bar-' + key).data('state', s.state); var progress_bar = $('#progress-bar-' + key);
if ($('#progress-bar-' + key).data('last_view_offset') != s.view_offset) { progress_bar.data('state', s.state);
$('#progress-bar-' + key).data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); 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 // Add temporary class so we know which instances are still active
@@ -771,13 +770,13 @@
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax); leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotal }, 250); scroller.animate({ left: leftTotal }, 250);
if (leftTotal == 0) { if (leftTotal === 0) {
$("#recently-added-page-left").addClass("disabled").blur(); $("#recently-added-page-left").addClass("disabled").blur();
} else { } else {
$("#recently-added-page-left").removeClass("disabled"); $("#recently-added-page-left").removeClass("disabled");
} }
if (leftTotal == leftMax) { if (leftTotal === leftMax) {
$("#recently-added-page-right").addClass("disabled").blur(); $("#recently-added-page-right").addClass("disabled").blur();
} else { } else {
$("#recently-added-page-right").removeClass("disabled"); $("#recently-added-page-right").removeClass("disabled");

View File

@@ -38,20 +38,21 @@ DOCUMENTATION :: END
<%! <%!
import re 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 # Get audio codec file
def af(codec): 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): if re.match(pattern, codec):
return file return file_type
return codec return codec
# Get audio codec file # Get audio codec file
def vf(codec): 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): if re.match(pattern, codec):
return file return file_type
return codec return codec
def br(text): def br(text):
@@ -356,7 +357,7 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-header"> <div class="table-card-header">
<div class="header-bar"> <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> </div>
<div class="table-card-back"> <div class="table-card-back">

View File

@@ -28,22 +28,15 @@ DOCUMENTATION :: END
% if data != None: % if data != None:
<% <%
from plexpy.common import MEDIA_TYPE_HEADERS
types = ('movie', 'show', 'artist', 'album') 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: % for media_type in types:
% if data['results_list'][media_type]: % if data['results_list'][media_type]:
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-header"> <div class="table-card-header">
<div class="header-bar"> <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> </div>
<div class="table-card-back"> <div class="table-card-back">

View File

@@ -28,9 +28,7 @@ libraries_list_table_options = {
$(td).html('<div class="edit-library-toggles">' + $(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>&nbsp' + '<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>&nbsp' +
'<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>&nbsp&nbsp&nbsp' + '<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>&nbsp&nbsp&nbsp' +
'<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>&nbsp' +
'<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>&nbsp' + '<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>&nbsp' +
'<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>&nbsp' +
'</div>'); '</div>');
}, },
"width": "7%", "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 row = libraries_list_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var do_notify = 0;
var do_notify_created = 0;
var keep_history = 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')) { if ($('#keep_history-' + rowData['section_id']).is(':checked')) {
keep_history = 1; keep_history = 1;
} }
@@ -280,8 +270,6 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
url: 'edit_library', url: 'edit_library',
data: { data: {
section_id: rowData['section_id'], section_id: rowData['section_id'],
do_notify: do_notify,
do_notify_created: do_notify_created,
keep_history: keep_history, keep_history: keep_history,
custom_thumb: custom_thumb custom_thumb: custom_thumb
}, },

View File

@@ -45,7 +45,6 @@ users_list_table_options = {
$(td).html('<div class="edit-user-toggles">' + $(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>&nbsp' + '<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>&nbsp' +
'<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>&nbsp&nbsp&nbsp' + '<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>&nbsp&nbsp&nbsp' +
'<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>&nbsp' +
'<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>&nbsp' + '<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>&nbsp' +
'<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>&nbsp' + '<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>&nbsp' +
'</div>'); '</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 row = users_list_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var do_notify = 0;
var keep_history = 0; var keep_history = 0;
var allow_guest = 0; var allow_guest = 0;
if ($('#do_notify-' + rowData['user_id']).is(':checked')) {
do_notify = 1;
}
if ($('#keep_history-' + rowData['user_id']).is(':checked')) { if ($('#keep_history-' + rowData['user_id']).is(':checked')) {
keep_history = 1; keep_history = 1;
} }
@@ -304,7 +299,6 @@ $('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > inpu
data: { data: {
user_id: rowData['user_id'], user_id: rowData['user_id'],
friendly_name: friendly_name, friendly_name: friendly_name,
do_notify: do_notify,
keep_history: keep_history, keep_history: keep_history,
allow_guest: allow_guest, allow_guest: allow_guest,
thumb: rowData['user_thumb'] thumb: rowData['user_thumb']

View File

@@ -63,7 +63,7 @@ DOCUMENTATION :: END
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
</div> </div>
% elif item['media_type'] == 'show': % 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">
<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-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">

View File

@@ -918,7 +918,7 @@
<div class="checkbox"> <div class="checkbox">
<label> <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> </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> <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> </div>

View File

@@ -39,7 +39,7 @@ DOCUMENTATION :: END
% if data: % if data:
<% <%
import plexpy from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
%> %>
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
@@ -85,8 +85,8 @@ DOCUMENTATION :: END
% if data['media_type'] != 'track': % if data['media_type'] != 'track':
<tr> <tr>
<td>Resolution</td> <td>Resolution</td>
<td>${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}</td> <td>${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['video_resolution'], data['video_resolution'])}</td>
</tr> </tr>
% endif % endif
<tr> <tr>
@@ -124,8 +124,8 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Container</td> <td>Container</td>
<td>${data['stream_container']}</td> <td>${data['stream_container'].upper()}</td>
<td>${data['container']}</td> <td>${data['container'].upper()}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -144,8 +144,8 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${data['stream_video_codec']}</td> <td>${data['stream_video_codec'].upper()} ${'(HW)' if data['transcode_hw_encoding'] else ''}</td>
<td>${data['video_codec']}</td> <td>${data['video_codec'].upper()} ${'(HW)' if data['transcode_hw_decoding'] else ''}</td>
</tr> </tr>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
@@ -189,8 +189,8 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${data['stream_audio_codec']}</td> <td>${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())}</td>
<td>${data['audio_codec']}</td> <td>${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())}</td>
</tr> </tr>
<tr> <tr>
<td>Bitrate</td> <td>Bitrate</td>
@@ -219,8 +219,8 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${data['stream_subtitle_codec']}</td> <td>${data['stream_subtitle_codec'].upper()}</td>
<td>${data['subtitle_codec']}</td> <td>${data['subtitle_codec'].upper()}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

2
lib/idna/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .package_data import __version__
from .core import *

118
lib/idna/codec.py Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

53
lib/idna/intranges.py Normal file
View 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
View File

@@ -0,0 +1,2 @@
__version__ = '2.6'

7634
lib/idna/uts46data.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -443,6 +443,7 @@ def dbcheck():
'transcode_protocol TEXT, transcode_container TEXT, ' 'transcode_protocol TEXT, transcode_container TEXT, '
'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,' 'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,'
'transcode_width INTEGER, transcode_height 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, ' 'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
'synced_version INTEGER, synced_version_profile 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, ' '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, ' 'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, ' 'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, ' '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_requested INTEGER, transcode_hw_full_pipeline INTEGER, '
'transcode_hw_decode_title TEXT, transcode_hw_encode TEXT, transcode_hw_encode_title TEXT, ' '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_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_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, ' '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' '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 # Upgrade session_history table from earlier versions
try: try:
c_db.execute('SELECT reference_id FROM session_history') 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 ' '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 # Upgrade users table from earlier versions
try: try:
c_db.execute('SELECT do_notify FROM users') c_db.execute('SELECT do_notify FROM users')
@@ -1340,8 +1391,8 @@ def dbcheck():
# Upgrade library_sections table from earlier versions (remove duplicated libraries) # Upgrade library_sections table from earlier versions (remove duplicated libraries)
try: try:
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""') result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""').fetchall()
if result.rowcount > 0: if len(result) > 0:
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.") logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
c_db.execute( c_db.execute(
'DELETE FROM library_sections WHERE server_id = ""' 'DELETE FROM library_sections WHERE server_id = ""'

View File

@@ -14,7 +14,7 @@
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import datetime import datetime
import threading import os
import time import time
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
@@ -26,7 +26,6 @@ import datafactory
import helpers import helpers
import logger import logger
import notification_handler import notification_handler
import notifiers
import pmsconnect import pmsconnect
@@ -75,9 +74,12 @@ class ActivityHandler(object):
monitor_proc.write_session(session=session, notify=False) monitor_proc.write_session(session=session, notify=False)
def on_start(self): def on_start(self):
if self.is_valid_session() and self.get_live_session(): if self.is_valid_session():
session = self.get_live_session() session = self.get_live_session()
if not session:
return
# Some DLNA clients create a new session temporarily when browsing the library # 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 # Wait and get session again to make sure it is an actual session
if session['platform'] == 'DLNA': if session['platform'] == 'DLNA':
@@ -124,6 +126,7 @@ class ActivityHandler(object):
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue" logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
% (str(self.get_session_key()), str(self.get_rating_key()))) % (str(self.get_session_key()), str(self.get_rating_key())))
ap.delete_session(session_key=self.get_session_key()) ap.delete_session(session_key=self.get_session_key())
delete_metadata_cache(self.get_session_key())
def on_pause(self, still_paused=False): def on_pause(self, still_paused=False):
if self.is_valid_session(): if self.is_valid_session():
@@ -436,12 +439,12 @@ def force_stop_stream(session_key):
ap.delete_session(session_key=session_key) ap.delete_session(session_key=session_key)
else: 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. " \ 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." "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) ap.increment_write_attempts(session_key=session_key)
# Reschedule for 30 seconds later # Reschedule for 30 seconds later
@@ -449,12 +452,13 @@ def force_stop_stream(session_key):
args=[session_key], seconds=30) args=[session_key], seconds=30)
else: 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." "Removing session from the database. Write attempt %s."
% (sessions['session_key'], sessions['rating_key'], str(sessions['write_attempts']))) % (session['session_key'], session['rating_key'], str(session['write_attempts'])))
logger.info(u"Tautulli Monitor :: Removing stale stream with sessionKey %s ratingKey %s from session queue" logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (sessions['session_key'], sessions['rating_key'])) % (session['session_key'], session['rating_key']))
ap.delete_session(session_key=session_key) ap.delete_session(session_key=session_key)
delete_metadata_cache(session_key)
def clear_recently_added_queue(rating_key): def clear_recently_added_queue(rating_key):
@@ -519,3 +523,11 @@ def on_created(rating_key, **kwargs):
else: else:
logger.error(u"Tautulli TimelineHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key)) 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))

View File

@@ -58,7 +58,7 @@ class ActivityProcessor(object):
'grandparent_thumb': session.get('grandparent_thumb', ''), 'grandparent_thumb': session.get('grandparent_thumb', ''),
'year': session.get('year', ''), 'year': session.get('year', ''),
'friendly_name': session.get('friendly_name', ''), 'friendly_name': session.get('friendly_name', ''),
#'ip_address': session.get('ip_address', ''), 'ip_address': session.get('ip_address', ''),
'player': session.get('player', ''), 'player': session.get('player', ''),
'platform': session.get('platform', ''), 'platform': session.get('platform', ''),
'parent_rating_key': session.get('parent_rating_key', ''), 'parent_rating_key': session.get('parent_rating_key', ''),
@@ -90,6 +90,8 @@ class ActivityProcessor(object):
'transcode_audio_channels': session.get('transcode_audio_channels', ''), 'transcode_audio_channels': session.get('transcode_audio_channels', ''),
'transcode_width': session.get('stream_video_width', ''), 'transcode_width': session.get('stream_video_width', ''),
'transcode_height': session.get('stream_video_height', ''), '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': session.get('synced_version', ''),
'synced_version_profile': session.get('synced_version_profile', ''), 'synced_version_profile': session.get('synced_version_profile', ''),
'optimized_version': session.get('optimized_version', ''), 'optimized_version': session.get('optimized_version', ''),
@@ -117,10 +119,6 @@ class ActivityProcessor(object):
'stopped': int(time.time()) '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', ''), keys = {'session_key': session.get('session_key', ''),
'rating_key': session.get('rating_key', '')} 'rating_key': session.get('rating_key', '')}
@@ -129,7 +127,6 @@ class ActivityProcessor(object):
if result == 'insert': if result == 'insert':
# Check if any notification agents have notifications enabled # Check if any notification agents have notifications enabled
if notify: if notify:
values.update({'ip_address': session.get('ip_address', 'N/A')})
plexpy.NOTIFY_QUEUE.put({'stream_data': values, 'notify_action': 'on_play'}) plexpy.NOTIFY_QUEUE.put({'stream_data': values, 'notify_action': 'on_play'})
# If it's our first write then time stamp it. # If it's our first write then time stamp it.
@@ -324,6 +321,7 @@ class ActivityProcessor(object):
'audio_codec': session['audio_codec'], 'audio_codec': session['audio_codec'],
'audio_bitrate': session['audio_bitrate'], 'audio_bitrate': session['audio_bitrate'],
'audio_channels': session['audio_channels'], 'audio_channels': session['audio_channels'],
'subtitle_codec': session['subtitle_codec'],
'transcode_protocol': session['transcode_protocol'], 'transcode_protocol': session['transcode_protocol'],
'transcode_container': session['transcode_container'], 'transcode_container': session['transcode_container'],
'transcode_video_codec': session['transcode_video_codec'], 'transcode_video_codec': session['transcode_video_codec'],
@@ -333,9 +331,11 @@ class ActivityProcessor(object):
'transcode_height': session['transcode_height'], 'transcode_height': session['transcode_height'],
'transcode_hw_requested': session['transcode_hw_requested'], 'transcode_hw_requested': session['transcode_hw_requested'],
'transcode_hw_full_pipeline': session['transcode_hw_full_pipeline'], '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_decode': session['transcode_hw_decode'],
'transcode_hw_encode': session['transcode_hw_encode'],
'transcode_hw_decode_title': session['transcode_hw_decode_title'], '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'], 'transcode_hw_encode_title': session['transcode_hw_encode_title'],
'stream_container': session['stream_container'], 'stream_container': session['stream_container'],
'stream_container_decision': session['stream_container_decision'], 'stream_container_decision': session['stream_container_decision'],

View File

@@ -32,130 +32,176 @@ DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png" DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
DEFAULT_ART = "interfaces/default/images/art.png" DEFAULT_ART = "interfaces/default/images/art.png"
PLATFORM_NAME_OVERRIDES = {'Konvergo': 'Plex Media Player', MEDIA_TYPE_HEADERS = {
'Mystery 3': 'Playstation 3', 'movie': 'Movies',
'Mystery 4': 'Playstation 4', 'show': 'TV Shows',
'Mystery 5': 'Xbox 360', 'season': 'Seasons',
'WebMAF': 'Playstation 4' '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', PMS_PLATFORM_NAME_OVERRIDES = {
'apple tv': 'atv', 'MacOSX': 'Mac'
'chrome': 'chrome', }
'chromecast': 'chromecast',
'dlna': 'dlna', PLATFORM_NAMES = {
'firefox': 'firefox', 'android': 'android',
'internet explorer': 'ie', 'apple tv': 'atv',
'ios': 'ios', 'chrome': 'chrome',
'ipad': 'ios', 'chromecast': 'chromecast',
'iphone': 'ios', 'dlna': 'dlna',
'kodi': 'kodi', 'firefox': 'firefox',
'linux': 'linux', 'internet explorer': 'ie',
'nexus': 'android', 'ios': 'ios',
'macos': 'macos', 'ipad': 'ios',
'microsoft edge': 'msedge', 'iphone': 'ios',
'opera': 'opera', 'kodi': 'kodi',
'osx': 'macos', 'linux': 'linux',
'playstation': 'playstation', 'nexus': 'android',
'plex home theater': 'plex', 'macos': 'macos',
'plex media player': 'plex', 'microsoft edge': 'msedge',
'plexamp': 'plexamp', 'opera': 'opera',
'plextogether': 'synclounge', 'osx': 'macos',
'roku': 'roku', 'playstation': 'playstation',
'safari': 'safari', 'plex home theater': 'plex',
'samsung': 'samsung', 'plex media player': 'plex',
'synclounge': 'synclounge', 'plexamp': 'plexamp',
'tivo': 'tivo', 'plextogether': 'synclounge',
'tvos': 'atv', 'roku': 'roku',
'vizio': 'opera', 'safari': 'safari',
'wiiu': 'wiiu', 'samsung': 'samsung',
'windows': 'windows', 'synclounge': 'synclounge',
'windows phone': 'wp', 'tivo': 'tivo',
'xbmc': 'xbmc', 'tvos': 'atv',
'xbox': 'xbox' '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)) PLATFORM_NAMES = OrderedDict(sorted(PLATFORM_NAMES.items(), key=lambda k: k[0], reverse=True))
MEDIA_FLAGS_AUDIO = {'ac.?3': 'dolby_digital', MEDIA_FLAGS_AUDIO = {
'truehd': 'dolby_truehd', 'ac.?3': 'dolby_digital',
'(dca|dta)': 'dts', 'truehd': 'dolby_truehd',
'dts(hd_|-hd|-)?ma': 'dca-ma', '(dca|dta)': 'dts',
'vorbis': 'ogg' 'dts(hd_|-hd|-)?ma': 'dca-ma',
} 'vorbis': 'ogg'
MEDIA_FLAGS_VIDEO = {'avc1': 'h264', }
'wmv(1|2)': 'wmv', MEDIA_FLAGS_VIDEO = {
'wmv3': 'wmvhd' 'avc1': 'h264',
} 'wmv(1|2)': 'wmv',
'wmv3': 'wmvhd'
}
AUDIO_CODEC_OVERRIDES = {'truehd': 'TrueHD'} AUDIO_CODEC_OVERRIDES = {
'truehd': 'TrueHD'
}
VIDEO_RESOLUTION_OVERRIDES = {'sd': 'SD', VIDEO_RESOLUTION_OVERRIDES = {
'480': '480p', 'sd': 'SD',
'540': '540p', '480': '480p',
'576': '576p', '540': '540p',
'720': '720p', '576': '576p',
'1080': '1080p', '720': '720p',
'4k': '4k' '1080': '1080p',
} '4k': '4k'
}
AUDIO_CHANNELS = {'1': 'Mono', AUDIO_CHANNELS = {
'2': 'Stereo', '1': 'Mono',
'3': '2.1', '2': 'Stereo',
'4': '3.1', '3': '2.1',
'6': '5.1', '4': '3.1',
'7': '6.1', '6': '5.1',
'8': '7.1' '7': '6.1',
} '8': '7.1'
}
VIDEO_QUALITY_PROFILES = {20000: '20 Mbps 1080p', VIDEO_QUALITY_PROFILES = {
12000: '12 Mbps 1080p', 20000: '20 Mbps 1080p',
10000: '10 Mbps 1080p', 12000: '12 Mbps 1080p',
8000: '8 Mbps 1080p', 10000: '10 Mbps 1080p',
4000: '4 Mbps 720p', 8000: '8 Mbps 1080p',
3000: '3 Mbps 720p', 4000: '4 Mbps 720p',
2000: '2 Mbps 720p', 3000: '3 Mbps 720p',
1500: '1.5 Mbps 480p', 2000: '2 Mbps 720p',
720: '0.7 Mbps 328p', 1500: '1.5 Mbps 480p',
320: '0.3 Mbps 240p', 720: '0.7 Mbps 328p',
208: '0.2 Mbps 160p', 320: '0.3 Mbps 240p',
96: '0.096 Mbps', 208: '0.2 Mbps 160p',
64: '0.064 Mbps' 96: '0.096 Mbps',
} 64: '0.064 Mbps'
}
VIDEO_QUALITY_PROFILES = OrderedDict(sorted(VIDEO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True)) VIDEO_QUALITY_PROFILES = OrderedDict(sorted(VIDEO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True))
AUDIO_QUALITY_PROFILES = {512: '512 kbps', AUDIO_QUALITY_PROFILES = {
320: '320 kbps', 512: '512 kbps',
256: '256 kbps', 320: '320 kbps',
192: '192 kbps', 256: '256 kbps',
128: '128 kbps', 192: '192 kbps',
96: '96 kbps' 128: '128 kbps',
} 96: '96 kbps'
}
AUDIO_QUALITY_PROFILES = OrderedDict(sorted(AUDIO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True)) AUDIO_QUALITY_PROFILES = OrderedDict(sorted(AUDIO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True))
SCHEDULER_LIST = ['Check GitHub for updates', HW_DECODERS = [
'Check for active sessions', 'dxva2',
'Check for recently added items', 'videotoolbox',
'Check for Plex updates', 'mediacodecndk',
'Check for Plex remote access', 'vaapi'
'Check server response', ]
'Refresh users list', HW_ENCODERS = [
'Refresh libraries list', 'qsv',
'Refresh Plex server URLs', 'nvenc',
'Backup Tautulli database', 'mf',
'Backup Tautulli config' '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 = [ DATE_TIME_FORMATS = [
{ {
'category': 'Year', 'category': 'Year',
'parameters': [
{'value': 'YYYY', 'description': 'Numeric, four digits', 'example': '1999, 2003'},
{'value': 'YY', 'description': 'Numeric, two digits', 'example': '99, 03'}
]
},
{
'category': 'Month',
'parameters': [ 'parameters': [
{'value': 'MMMM', 'description': 'Textual, full', 'example': 'January-December'}, {'value': 'MMMM', 'description': 'Textual, full', 'example': 'January-December'},
{'value': 'MMM', 'description': 'Textual, three letters', 'example': 'Jan-Dec'}, {'value': 'MMM', 'description': 'Textual, three letters', 'example': 'Jan-Dec'},
{'value': 'MM', 'description': 'Numeric, with leading zeros', 'example': '42747'}, {'value': 'MM', 'description': 'Numeric, with leading zeros', 'example': '01-12'},
{'value': 'M', 'description': 'Numeric, without leading zeros', 'example': '42747'}, {'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': [ 'parameters': [
{'value': 'DDDD', 'description': 'Numeric, with leading zeros', 'example': '001-365'}, {'value': 'DDDD', 'description': 'Numeric, with leading zeros', 'example': '001-365'},
{'value': 'DDD', 'description': 'Numeric, without leading zeros', 'example': '1-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', 'category': 'Day of the Month',
'parameters': [ 'parameters': [
{'value': 'DD', 'description': 'Numeric, with leading zeros', 'example': '42766'}, {'value': 'DD', 'description': 'Numeric, with leading zeros', 'example': '01-31'},
{'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '42766'}, {'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '1-31'},
{'value': 'Do', 'description': 'Numeric, with suffix', 'example': 'E.g. 1st, 2nd ... 31st.'}, {'value': 'Do', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 31st'},
] ]
}, },
{ {
@@ -178,7 +225,9 @@ DATE_TIME_FORMATS = [
'parameters': [ 'parameters': [
{'value': 'dddd', 'description': 'Textual, full', 'example': 'Sunday-Saturday'}, {'value': 'dddd', 'description': 'Textual, full', 'example': 'Sunday-Saturday'},
{'value': 'ddd', 'description': 'Textual, three letters', 'example': 'Sun-Sat'}, {'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': 'd', 'description': 'Numeric', 'example': '0-6'},
{'value': 'do', 'description': 'Numeric, with suffix', 'example': '0th, 1st ... 6th'},
] ]
}, },
{ {
@@ -186,8 +235,8 @@ DATE_TIME_FORMATS = [
'parameters': [ 'parameters': [
{'value': 'HH', 'description': '24-hour, with leading zeros', 'example': '00-23'}, {'value': 'HH', 'description': '24-hour, with leading zeros', 'example': '00-23'},
{'value': 'H', 'description': '24-hour, without leading zeros', 'example': '0-23'}, {'value': 'H', 'description': '24-hour, without leading zeros', 'example': '0-23'},
{'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '42747'}, {'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '01-12'},
{'value': 'h', 'description': '12-hour, without leading zeros', 'example': '42747'}, {'value': 'h', 'description': '12-hour, without leading zeros', 'example': '1-12'},
] ]
}, },
{ {
@@ -214,8 +263,8 @@ DATE_TIME_FORMATS = [
{ {
'category': 'Timezone', 'category': 'Timezone',
'parameters': [ 'parameters': [
{'value': 'ZZ', 'description': 'UTC offset', 'example': 'E.g. +0100, -0700'}, {'value': 'ZZ', 'description': 'UTC offset', 'example': '+0100, -0700'},
{'value': 'Z', 'description': 'UTC offset', 'example': 'E.g. +01:00, -07:00'}, {'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'}, {'value': 'X', 'description': 'Unix timestamp', 'example': 'E.g. 1456887825'},
] ]
}, },
] ]
NOTIFICATION_PARAMETERS = [ 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 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 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 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': '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': '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.'}, {'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.'}, {'name': 'Plexpy Update Changelog', 'type': 'int', 'value': 'plexpy_update_changelog', 'description': 'The changelog for the available update.'},
] ]
}, },
] ]

View File

@@ -882,6 +882,7 @@ class DataFactory(object):
'stream_video_framerate, ' \ 'stream_video_framerate, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \ 'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \ 'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
'session_history_metadata.media_type, title, grandparent_title ' \ 'session_history_metadata.media_type, title, grandparent_title ' \
'FROM session_history_media_info ' \ 'FROM session_history_media_info ' \
'JOIN session_history ON session_history_media_info.id = session_history.id ' \ 'JOIN session_history ON session_history_media_info.id = session_history.id ' \
@@ -899,6 +900,7 @@ class DataFactory(object):
'stream_video_framerate, ' \ 'stream_video_framerate, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \ 'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \ 'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
'media_type, title, grandparent_title ' \ 'media_type, title, grandparent_title ' \
'FROM sessions ' \ 'FROM sessions ' \
'WHERE session_key = ? %s' % user_cond 'WHERE session_key = ? %s' % user_cond
@@ -945,11 +947,14 @@ class DataFactory(object):
'subtitles': item['subtitles'], 'subtitles': item['subtitles'],
'stream_subtitle_decision': item['stream_subtitle_decision'], 'stream_subtitle_decision': item['stream_subtitle_decision'],
'stream_subtitle_codec': item['stream_subtitle_codec'], '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'], 'media_type': item['media_type'],
'title': item['title'], 'title': item['title'],
'grandparent_title': item['grandparent_title'] 'grandparent_title': item['grandparent_title']
} }
stream_output = {k: v or '' for k, v in stream_output.iteritems()}
return stream_output return stream_output
def get_metadata_details(self, rating_key): def get_metadata_details(self, rating_key):

View File

@@ -154,7 +154,7 @@ class HTTPHandler(object):
try: try:
if self.output_format == 'text': if self.output_format == 'text':
output = response_content.decode('utf-8', 'ignore') output = response_content.decode('utf-8', 'ignore')
if self.output_format == 'dict': elif self.output_format == 'dict':
output = helpers.convert_xml_to_dict(response_content) output = helpers.convert_xml_to_dict(response_content)
elif self.output_format == 'json': elif self.output_format == 'json':
output = helpers.convert_xml_to_json(response_content) output = helpers.convert_xml_to_json(response_content)

View File

@@ -16,7 +16,7 @@
import arrow import arrow
import bleach import bleach
from collections import Counter from collections import Counter, defaultdict
from itertools import groupby from itertools import groupby
import json import json
from operator import itemgetter from operator import itemgetter
@@ -131,19 +131,19 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
if stream_data: if stream_data:
# Check if notifications enabled for user and library # Check if notifications enabled for user and library
user_data = users.Users() # user_data = users.Users()
user_details = user_data.get_details(user_id=stream_data['user_id']) # 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() # if not user_details['do_notify']:
library_details = library_data.get_details(section_id=stream_data['section_id']) # logger.debug(u"Tautulli NotificationHandler :: Notifications for user '%s' are disabled." % user_details['username'])
# return False
if not user_details['do_notify']: #
logger.debug(u"Tautulli NotificationHandler :: Notifications for user '%s' are disabled." % user_details['username']) # elif not library_details['do_notify'] and notify_action not in ('on_concurrent', 'on_newdevice'):
return False # logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' are disabled." % library_details['section_name'])
# 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': if notify_action == 'on_concurrent':
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
@@ -188,12 +188,12 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
elif timeline_data: elif timeline_data:
# Check if notifications enabled for library # Check if notifications enabled for library
library_data = libraries.Libraries() # library_data = libraries.Libraries()
library_details = library_data.get_details(section_id=timeline_data['section_id']) # library_details = library_data.get_details(section_id=timeline_data['section_id'])
#
if not library_details['do_notify_created']: # if not library_details['do_notify_created']:
# logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' is disabled." % library_details['section_name']) # # logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' is disabled." % library_details['section_name'])
return False # return False
return True return True
@@ -449,17 +449,20 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
elif timeline: elif timeline:
rating_key = timeline['rating_key'] rating_key = timeline['rating_key']
pms_connect = pmsconnect.PmsConnect() notify_params = defaultdict(str)
metadata = pms_connect.get_metadata_details(rating_key=rating_key) 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: if timeline:
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key)) notify_params.update(timeline)
return None
## TODO: Check list of media info items, currently only grabs first item ## TODO: Check list of media info items, currently only grabs first item
media_info = media_part_info = {} media_info = media_part_info = {}
if 'media_info' in metadata and len(metadata['media_info']) > 0: if 'media_info' in notify_params and len(notify_params['media_info']) > 0:
media_info = metadata['media_info'][0] media_info = notify_params['media_info'][0]
if 'parts' in media_info and len(media_info['parts']) > 0: if 'parts' in media_info and len(media_info['parts']) > 0:
media_part_info = media_info.pop('parts')[0] media_part_info = media_info.pop('parts')[0]
@@ -476,11 +479,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
media_part_info.update(stream) media_part_info.update(stream)
stream_subtitle = True stream_subtitle = True
notify_params.update(media_info)
notify_params.update(media_part_info)
child_metadata = grandchild_metadata = [] child_metadata = grandchild_metadata = []
for key in kwargs.pop('child_keys', []): 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', []): 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 values
session = session or {} session = session or {}
@@ -507,102 +513,102 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
stream_duration = 0 stream_duration = 0
view_offset = helpers.convert_milliseconds_to_minutes(session.get('view_offset', 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 remaining_duration = duration - view_offset
# Build Plex URL # 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%2Fnotify_params%2F{rating_key}'.format(
web_url=plexpy.CONFIG.PMS_WEB_URL, web_url=plexpy.CONFIG.PMS_WEB_URL,
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER, pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
rating_key=rating_key) rating_key=rating_key)
# Get media IDs from guid and build URLs # Get media IDs from guid and build URLs
if 'imdb://' in metadata['guid']: if 'imdb://' in notify_params['guid']:
metadata['imdb_id'] = metadata['guid'].split('imdb://')[1].split('?')[0] notify_params['imdb_id'] = notify_params['guid'].split('imdb://')[1].split('?')[0]
metadata['imdb_url'] = 'https://www.imdb.com/title/' + metadata['imdb_id'] notify_params['imdb_url'] = 'https://www.imdb.com/title/' + notify_params['imdb_id']
metadata['trakt_url'] = 'https://trakt.tv/search/imdb/' + metadata['imdb_id'] notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
if 'thetvdb://' in metadata['guid']: if 'thetvdb://' in notify_params['guid']:
metadata['thetvdb_id'] = metadata['guid'].split('thetvdb://')[1].split('/')[0] notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdb://')[1].split('/')[0]
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id'] notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show' notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
elif 'thetvdbdvdorder://' in metadata['guid']: elif 'thetvdbdvdorder://' in notify_params['guid']:
metadata['thetvdb_id'] = metadata['guid'].split('thetvdbdvdorder://')[1].split('/')[0] notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0]
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id'] notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show' notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?id_type=show'
if 'themoviedb://' in metadata['guid']: if 'themoviedb://' in notify_params['guid']:
if metadata['media_type'] == 'movie': if notify_params['media_type'] == 'movie':
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('?')[0] notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('?')[0]
metadata['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + metadata['themoviedb_id'] notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id']
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=movie' notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=movie'
elif metadata['media_type'] in ('show', 'season', 'episode'): elif notify_params['media_type'] in ('show', 'season', 'episode'):
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('/')[0] notify_params['themoviedb_id'] = notify_params['guid'].split('themoviedb://')[1].split('/')[0]
metadata['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + metadata['themoviedb_id'] notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id']
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=show' notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?id_type=show'
if 'lastfm://' in metadata['guid']: if 'lastfm://' in notify_params['guid']:
metadata['lastfm_id'] = metadata['guid'].split('lastfm://')[1].rsplit('/', 1)[0] notify_params['lastfm_id'] = notify_params['guid'].split('lastfm://')[1].rsplit('/', 1)[0]
metadata['lastfm_url'] = 'https://www.last.fm/music/' + metadata['lastfm_id'] notify_params['lastfm_url'] = 'https://www.last.fm/music/' + notify_params['lastfm_id']
# Get TheMovieDB info # Get TheMovieDB info
if plexpy.CONFIG.THEMOVIEDB_LOOKUP: 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, themoveidb_json = get_themoviedb_info(rating_key=rating_key,
media_type=metadata['media_type'], media_type=notify_params['media_type'],
themoviedb_id=metadata['themoviedb_id']) themoviedb_id=notify_params['themoviedb_id'])
if themoveidb_json.get('imdb_id'): if themoveidb_json.get('imdb_id'):
metadata['imdb_id'] = themoveidb_json['imdb_id'] notify_params['imdb_id'] = themoveidb_json['imdb_id']
metadata['imdb_url'] = 'https://www.imdb.com/title/' + 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, themoviedb_info = lookup_themoviedb_by_id(rating_key=rating_key,
thetvdb_id=metadata.get('thetvdb_id'), thetvdb_id=notify_params.get('thetvdb_id'),
imdb_id=metadata.get('imdb_id')) imdb_id=notify_params.get('imdb_id'))
metadata.update(themoviedb_info) notify_params.update(themoviedb_info)
# Get TVmaze info (for tv shows only) # Get TVmaze info (for tv shows only)
if plexpy.CONFIG.TVMAZE_LOOKUP: 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, tvmaze_info = lookup_tvmaze_by_id(rating_key=rating_key,
thetvdb_id=metadata.get('thetvdb_id'), thetvdb_id=notify_params.get('thetvdb_id'),
imdb_id=metadata.get('imdb_id')) imdb_id=notify_params.get('imdb_id'))
metadata.update(tvmaze_info) notify_params.update(tvmaze_info)
if tvmaze_info.get('thetvdb_id'): 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'): 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'): if notify_params['media_type'] in ('movie', 'show', 'artist'):
poster_thumb = metadata['thumb'] poster_thumb = notify_params['thumb']
poster_key = metadata['rating_key'] poster_key = notify_params['rating_key']
poster_title = metadata['title'] poster_title = notify_params['title']
elif metadata['media_type'] in ('season', 'album'): elif notify_params['media_type'] in ('season', 'album'):
poster_thumb = metadata['thumb'] or metadata['parent_thumb'] poster_thumb = notify_params['thumb'] or notify_params['parent_thumb']
poster_key = metadata['rating_key'] poster_key = notify_params['rating_key']
poster_title = '%s - %s' % (metadata['parent_title'], poster_title = '%s - %s' % (notify_params['parent_title'],
metadata['title']) notify_params['title'])
elif metadata['media_type'] in ('episode', 'track'): elif notify_params['media_type'] in ('episode', 'track'):
poster_thumb = metadata['parent_thumb'] or metadata['grandparent_thumb'] poster_thumb = notify_params['parent_thumb'] or notify_params['grandparent_thumb']
poster_key = metadata['parent_rating_key'] poster_key = notify_params['parent_rating_key']
poster_title = '%s - %s' % (metadata['grandparent_title'], poster_title = '%s - %s' % (notify_params['grandparent_title'],
metadata['parent_title']) notify_params['parent_title'])
else: else:
poster_thumb = '' poster_thumb = ''
if plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS: if plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS:
poster_info = get_poster_info(poster_thumb=poster_thumb, poster_key=poster_key, poster_title=poster_title) 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) if ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT)
and metadata['media_type'] in ('show', 'artist')): and notify_params['media_type'] in ('show', 'artist')):
show_name = metadata['title'] show_name = notify_params['title']
episode_name = '' episode_name = ''
artist_name = metadata['title'] artist_name = notify_params['title']
album_name = '' album_name = ''
track_name = '' track_name = ''
@@ -614,14 +620,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
track_num, track_num00 = '', '' track_num, track_num00 = '', ''
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT) elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
and metadata['media_type'] in ('season', 'album')): and notify_params['media_type'] in ('season', 'album')):
show_name = metadata['parent_title'] show_name = notify_params['parent_title']
episode_name = '' episode_name = ''
artist_name = metadata['parent_title'] artist_name = notify_params['parent_title']
album_name = metadata['title'] album_name = notify_params['title']
track_name = '' track_name = ''
season_num = metadata['media_index'].zfill(1) season_num = str(notify_params['media_index']).zfill(1)
season_num00 = metadata['media_index'].zfill(2) season_num00 = str(notify_params['media_index']).zfill(2)
num, num00 = format_group_index([helpers.cast_to_int(d['media_index']) num, num00 = format_group_index([helpers.cast_to_int(d['media_index'])
for d in child_metadata if d['parent_rating_key'] == rating_key]) for d in child_metadata if d['parent_rating_key'] == rating_key])
@@ -629,192 +635,196 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
track_num, track_num00 = num, num00 track_num, track_num00 = num, num00
else: else:
show_name = metadata['grandparent_title'] show_name = notify_params['grandparent_title']
episode_name = metadata['title'] episode_name = notify_params['title']
artist_name = metadata['grandparent_title'] artist_name = notify_params['grandparent_title']
album_name = metadata['parent_title'] album_name = notify_params['parent_title']
track_name = metadata['title'] track_name = notify_params['title']
season_num = metadata['parent_media_index'].zfill(1) season_num = str(notify_params['parent_media_index']).zfill(1)
season_num00 = metadata['parent_media_index'].zfill(2) season_num00 = str(notify_params['parent_media_index']).zfill(2)
episode_num = metadata['media_index'].zfill(1) episode_num = str(notify_params['media_index']).zfill(1)
episode_num00 = metadata['media_index'].zfill(2) episode_num00 = str(notify_params['media_index']).zfill(2)
track_num = metadata['media_index'].zfill(1) track_num = str(notify_params['media_index']).zfill(1)
track_num00 = metadata['media_index'].zfill(2) track_num00 = str(notify_params['media_index']).zfill(2)
available_params = {# Global paramaters available_params = {
'plexpy_version': common.VERSION_NUMBER, # Global paramaters
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH, 'plexpy_version': common.VERSION_NUMBER,
'plexpy_commit': plexpy.CURRENT_VERSION, 'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
'server_name': server_name, 'plexpy_commit': plexpy.CURRENT_VERSION,
'server_uptime': server_uptime, 'server_name': server_name,
'server_version': server_times.get('version',''), 'server_uptime': server_uptime,
'action': notify_action.split('on_')[-1], 'server_version': server_times.get('version', ''),
'datestamp': arrow.now().format(date_format), 'action': notify_action.lstrip('on_'),
'timestamp': arrow.now().format(time_format), 'datestamp': arrow.now().format(date_format),
# Stream parameters 'timestamp': arrow.now().format(time_format),
'streams': stream_count, # Stream parameters
'user_streams': user_stream_count, 'streams': stream_count,
'user': session.get('friendly_name',''), 'user_streams': user_stream_count,
'username': session.get('user',''), 'user': notify_params['friendly_name'],
'device': session.get('device',''), 'username': notify_params['user'],
'platform': session.get('platform',''), 'device': notify_params['device'],
'product': session.get('product',''), 'platform': notify_params['platform'],
'player': session.get('player',''), 'product': notify_params['product'],
'ip_address': session.get('ip_address','N/A'), 'player': notify_params['player'],
'stream_duration': stream_duration, 'ip_address': notify_params.get('ip_address', 'N/A'),
'stream_time': arrow.get(stream_duration * 60).format(duration_format), 'stream_duration': stream_duration,
'remaining_duration': remaining_duration, 'stream_time': arrow.get(stream_duration * 60).format(duration_format),
'remaining_time': arrow.get(remaining_duration * 60).format(duration_format), 'remaining_duration': remaining_duration,
'progress_duration': view_offset, 'remaining_time': arrow.get(remaining_duration * 60).format(duration_format),
'progress_time': arrow.get(view_offset * 60).format(duration_format), 'progress_duration': view_offset,
'progress_percent': helpers.get_percent(view_offset, duration), 'progress_time': arrow.get(view_offset * 60).format(duration_format),
'transcode_decision': transcode_decision, 'progress_percent': helpers.get_percent(view_offset, duration),
'video_decision': session.get('video_decision',''), 'transcode_decision': transcode_decision,
'audio_decision': session.get('audio_decision',''), 'video_decision': notify_params['video_decision'],
'subtitle_decision': session.get('subtitle_decision',''), 'audio_decision': notify_params['audio_decision'],
'quality_profile': session.get('quality_profile',''), 'subtitle_decision': notify_params['subtitle_decision'],
'optimized_version': session.get('optimized_version',''), 'quality_profile': notify_params['quality_profile'],
'optimized_version_profile': session.get('optimized_version_profile',''), 'optimized_version': notify_params['optimized_version'],
'stream_local': session.get('local', ''), 'optimized_version_profile': notify_params['optimized_version_profile'],
'stream_location': session.get('location', ''), 'synced_version': notify_params['synced_version'],
'stream_bandwidth': session.get('bandwidth', ''), 'stream_local': notify_params['local'],
'stream_container': session.get('stream_container', ''), 'stream_location': notify_params['location'],
'stream_bitrate': session.get('stream_bitrate', ''), 'stream_bandwidth': notify_params['bandwidth'],
'stream_aspect_ratio': session.get('stream_aspect_ratio', ''), 'stream_container': notify_params['stream_container'],
'stream_video_codec': session.get('stream_video_codec', ''), 'stream_bitrate': notify_params['stream_bitrate'],
'stream_video_codec_level': session.get('stream_video_codec_level', ''), 'stream_aspect_ratio': notify_params['stream_aspect_ratio'],
'stream_video_bitrate': session.get('stream_video_bitrate', ''), 'stream_video_codec': notify_params['stream_video_codec'],
'stream_video_bit_depth': session.get('stream_video_bit_depth', ''), 'stream_video_codec_level': notify_params['stream_video_codec_level'],
'stream_video_framerate': session.get('stream_video_framerate', ''), 'stream_video_bitrate': notify_params['stream_video_bitrate'],
'stream_video_ref_frames': session.get('stream_video_ref_frames', ''), 'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
'stream_video_resolution': session.get('stream_video_resolution', ''), 'stream_video_framerate': notify_params['stream_video_framerate'],
'stream_video_height': session.get('stream_video_height', ''), 'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
'stream_video_width': session.get('stream_video_width', ''), 'stream_video_resolution': notify_params['stream_video_resolution'],
'stream_video_language': session.get('stream_video_language', ''), 'stream_video_height': notify_params['stream_video_height'],
'stream_video_language_code': session.get('stream_video_language_code', ''), 'stream_video_width': notify_params['stream_video_width'],
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''), 'stream_video_language': notify_params['stream_video_language'],
'stream_audio_bitrate_mode': session.get('stream_audio_bitrate_mode', ''), 'stream_video_language_code': notify_params['stream_video_language_code'],
'stream_audio_codec': session.get('stream_audio_codec', ''), 'stream_audio_bitrate': notify_params['stream_audio_bitrate'],
'stream_audio_channels': session.get('stream_audio_channels', ''), 'stream_audio_bitrate_mode': notify_params['stream_audio_bitrate_mode'],
'stream_audio_channel_layout': session.get('stream_audio_channel_layout', ''), 'stream_audio_codec': notify_params['stream_audio_codec'],
'stream_audio_sample_rate': session.get('stream_audio_sample_rate', ''), 'stream_audio_channels': notify_params['stream_audio_channels'],
'stream_audio_language': session.get('stream_audio_language', ''), 'stream_audio_channel_layout': notify_params['stream_audio_channel_layout'],
'stream_audio_language_code': session.get('stream_audio_language_code', ''), 'stream_audio_sample_rate': notify_params['stream_audio_sample_rate'],
'stream_subtitle_codec': session.get('stream_subtitle_codec', ''), 'stream_audio_language': notify_params['stream_audio_language'],
'stream_subtitle_container': session.get('stream_subtitle_container', ''), 'stream_audio_language_code': notify_params['stream_audio_language_code'],
'stream_subtitle_format': session.get('stream_subtitle_format', ''), 'stream_subtitle_codec': notify_params['stream_subtitle_codec'],
'stream_subtitle_forced': session.get('stream_subtitle_forced', ''), 'stream_subtitle_container': notify_params['stream_subtitle_container'],
'stream_subtitle_language': session.get('stream_subtitle_language', ''), 'stream_subtitle_format': notify_params['stream_subtitle_format'],
'stream_subtitle_language_code': session.get('stream_subtitle_language_code', ''), 'stream_subtitle_forced': notify_params['stream_subtitle_forced'],
'stream_subtitle_location': session.get('stream_subtitle_location', ''), 'stream_subtitle_language': notify_params['stream_subtitle_language'],
'transcode_container': session.get('transcode_container',''), 'stream_subtitle_language_code': notify_params['stream_subtitle_language_code'],
'transcode_video_codec': session.get('transcode_video_codec',''), 'stream_subtitle_location': notify_params['stream_subtitle_location'],
'transcode_video_width': session.get('transcode_width',''), 'transcode_container': notify_params['transcode_container'],
'transcode_video_height': session.get('transcode_height',''), 'transcode_video_codec': notify_params['transcode_video_codec'],
'transcode_audio_codec': session.get('transcode_audio_codec',''), 'transcode_video_width': notify_params['transcode_width'],
'transcode_audio_channels': session.get('transcode_audio_channels',''), 'transcode_video_height': notify_params['transcode_height'],
'transcode_hw_requested': session.get('transcode_hw_requested',''), 'transcode_audio_codec': notify_params['transcode_audio_codec'],
'transcode_hw_decode': session.get('transcode_hw_decode',''), 'transcode_audio_channels': notify_params['transcode_audio_channels'],
'transcode_hw_decode_title': session.get('transcode_hw_decode_title',''), 'transcode_hw_requested': notify_params['transcode_hw_requested'],
'transcode_hw_encode': session.get('transcode_hw_encode',''), 'transcode_hw_decoding': notify_params['transcode_hw_decoding'],
'transcode_hw_encode_title': session.get('transcode_hw_encode_title',''), 'transcode_hw_decode_codec': notify_params['transcode_hw_decode'],
'transcode_hw_full_pipeline': session.get('transcode_hw_full_pipeline',''), 'transcode_hw_decode_title': notify_params['transcode_hw_decode_title'],
'session_key': session.get('session_key',''), 'transcode_hw_encoding': notify_params['transcode_hw_encoding'],
'transcode_key': session.get('transcode_key',''), 'transcode_hw_encode_codec': notify_params['transcode_hw_encode'],
'session_id': session.get('session_id',''), 'transcode_hw_encode_title': notify_params['transcode_hw_encode_title'],
'user_id': session.get('user_id',''), 'transcode_hw_full_pipeline': notify_params['transcode_hw_full_pipeline'],
'machine_id': session.get('machine_id',''), 'session_key': notify_params['session_key'],
# Source metadata parameters 'transcode_key': notify_params['transcode_key'],
'media_type': metadata['media_type'], 'session_id': notify_params['session_id'],
'title': metadata['full_title'], 'user_id': notify_params['user_id'],
'library_name': metadata['library_name'], 'machine_id': notify_params['machine_id'],
'show_name': show_name, # Source metadata parameters
'episode_name': episode_name, 'media_type': notify_params['media_type'],
'artist_name': artist_name, 'title': notify_params['full_title'],
'album_name': album_name, 'library_name': notify_params['library_name'],
'track_name': track_name, 'show_name': show_name,
'season_num': season_num, 'episode_name': episode_name,
'season_num00': season_num00, 'artist_name': artist_name,
'episode_num': episode_num, 'album_name': album_name,
'episode_num00': episode_num00, 'track_name': track_name,
'track_num': track_num, 'season_num': season_num,
'track_num00': track_num00, 'season_num00': season_num00,
'year': metadata['year'], 'episode_num': episode_num,
'release_date': arrow.get(metadata['originally_available_at']).format(date_format) 'episode_num00': episode_num00,
if metadata['originally_available_at'] else '', 'track_num': track_num,
'air_date': arrow.get(metadata['originally_available_at']).format(date_format) 'track_num00': track_num00,
if metadata['originally_available_at'] else '', 'year': notify_params['year'],
'added_date': arrow.get(metadata['added_at']).format(date_format) 'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
if metadata['added_at'] else '', if notify_params['originally_available_at'] else '',
'updated_date': arrow.get(metadata['updated_at']).format(date_format) 'air_date': arrow.get(notify_params['originally_available_at']).format(date_format)
if metadata['updated_at'] else '', if notify_params['originally_available_at'] else '',
'last_viewed_date': arrow.get(metadata['last_viewed_at']).format(date_format) 'added_date': arrow.get(notify_params['added_at']).format(date_format)
if metadata['last_viewed_at'] else '', if notify_params['added_at'] else '',
'studio': metadata['studio'], 'updated_date': arrow.get(notify_params['updated_at']).format(date_format)
'content_rating': metadata['content_rating'], if notify_params['updated_at'] else '',
'directors': ', '.join(metadata['directors']), 'last_viewed_date': arrow.get(notify_params['last_viewed_at']).format(date_format)
'writers': ', '.join(metadata['writers']), if notify_params['last_viewed_at'] else '',
'actors': ', '.join(metadata['actors']), 'studio': notify_params['studio'],
'genres': ', '.join(metadata['genres']), 'content_rating': notify_params['content_rating'],
'summary': metadata['summary'], 'directors': ', '.join(notify_params['directors']),
'tagline': metadata['tagline'], 'writers': ', '.join(notify_params['writers']),
'rating': metadata['rating'], 'actors': ', '.join(notify_params['actors']),
'audience_rating': helpers.get_percent(metadata['audience_rating'], 10) or '', 'genres': ', '.join(notify_params['genres']),
'duration': duration, 'summary': notify_params['summary'],
'poster_title': metadata.get('poster_title',''), 'tagline': notify_params['tagline'],
'poster_url': metadata.get('poster_url',''), 'rating': notify_params['rating'],
'plex_url': metadata.get('plex_url',''), 'audience_rating': helpers.get_percent(notify_params['audience_rating'], 10) or '',
'imdb_id': metadata.get('imdb_id',''), 'duration': duration,
'imdb_url': metadata.get('imdb_url',''), 'poster_title': notify_params['poster_title'],
'thetvdb_id': metadata.get('thetvdb_id',''), 'poster_url': notify_params['poster_url'],
'thetvdb_url': metadata.get('thetvdb_url',''), 'plex_url': notify_params['plex_url'],
'themoviedb_id': metadata.get('themoviedb_id',''), 'imdb_id': notify_params['imdb_id'],
'themoviedb_url': metadata.get('themoviedb_url',''), 'imdb_url': notify_params['imdb_url'],
'tvmaze_id': metadata.get('tvmaze_id',''), 'thetvdb_id': notify_params['thetvdb_id'],
'tvmaze_url': metadata.get('tvmaze_url',''), 'thetvdb_url': notify_params['thetvdb_url'],
'lastfm_url': metadata.get('lastfm_url',''), 'themoviedb_id': notify_params['themoviedb_id'],
'trakt_url': metadata.get('trakt_url',''), 'themoviedb_url': notify_params['themoviedb_url'],
'container': session.get('container', media_info.get('container','')), 'tvmaze_id': notify_params['tvmaze_id'],
'bitrate': session.get('bitrate', media_info.get('bitrate','')), 'tvmaze_url': notify_params['tvmaze_url'],
'aspect_ratio': session.get('aspect_ratio', media_info.get('aspect_ratio','')), 'lastfm_url': notify_params['lastfm_url'],
'video_codec': session.get('video_codec', media_part_info.get('video_codec','')), 'trakt_url': notify_params['trakt_url'],
'video_codec_level': session.get('video_codec_level', media_part_info.get('video_codec_level','')), 'container': notify_params['container'],
'video_bitrate': session.get('video_bitrate', media_part_info.get('video_bitrate','')), 'bitrate': notify_params['bitrate'],
'video_bit_depth': session.get('video_bit_depth', media_part_info.get('video_bit_depth','')), 'aspect_ratio': notify_params['aspect_ratio'],
'video_framerate': session.get('video_framerate', media_info.get('video_framerate','')), 'video_codec': notify_params['video_codec'],
'video_ref_frames': session.get('video_ref_frames', media_part_info.get('video_ref_frames','')), 'video_codec_level': notify_params['video_codec_level'],
'video_resolution': session.get('video_resolution', media_info.get('video_resolution','')), 'video_bitrate': notify_params['video_bitrate'],
'video_height': session.get('height', media_info.get('height','')), 'video_bit_depth': notify_params['video_bit_depth'],
'video_width': session.get('width', media_info.get('width','')), 'video_framerate': notify_params['video_framerate'],
'video_language': session.get('video_language', media_part_info.get('video_language','')), 'video_ref_frames': notify_params['video_ref_frames'],
'video_language_code': session.get('video_language_code', media_part_info.get('video_language_code','')), 'video_resolution': notify_params['video_resolution'],
'audio_bitrate': session.get('audio_bitrate', media_part_info.get('audio_bitrate','')), 'video_height': notify_params['height'],
'audio_bitrate_mode': session.get('audio_bitrate_mode', media_part_info.get('audio_bitrate_mode','')), 'video_width': notify_params['width'],
'audio_codec': session.get('audio_codec', media_part_info.get('audio_codec','')), 'video_language': notify_params['video_language'],
'audio_channels': session.get('audio_channels', media_part_info.get('audio_channels','')), 'video_language_code': notify_params['video_language_code'],
'audio_channel_layout': session.get('audio_channel_layout', media_part_info.get('audio_channel_layout','')), 'audio_bitrate': notify_params['audio_bitrate'],
'audio_sample_rate': session.get('audio_sample_rate', media_part_info.get('audio_sample_rate','')), 'audio_bitrate_mode': notify_params['audio_bitrate_mode'],
'audio_language': session.get('audio_language', media_part_info.get('audio_language','')), 'audio_codec': notify_params['audio_codec'],
'audio_language_code': session.get('audio_language_code', media_part_info.get('audio_language_code','')), 'audio_channels': notify_params['audio_channels'],
'subtitle_codec': session.get('subtitle_codec', media_part_info.get('subtitle_codec','')), 'audio_channel_layout': notify_params['audio_channel_layout'],
'subtitle_container': session.get('subtitle_container', media_part_info.get('subtitle_container','')), 'audio_sample_rate': notify_params['audio_sample_rate'],
'subtitle_format': session.get('subtitle_format', media_part_info.get('subtitle_format','')), 'audio_language': notify_params['audio_language'],
'subtitle_forced': session.get('subtitle_forced', media_part_info.get('subtitle_forced','')), 'audio_language_code': notify_params['audio_language_code'],
'subtitle_location': session.get('subtitle_location', media_part_info.get('subtitle_location','')), 'subtitle_codec': notify_params['subtitle_codec'],
'subtitle_language': session.get('subtitle_language', media_part_info.get('subtitle_language','')), 'subtitle_container': notify_params['subtitle_container'],
'subtitle_language_code': session.get('subtitle_language_code', media_part_info.get('subtitle_language_code','')), 'subtitle_format': notify_params['subtitle_format'],
'file': media_part_info.get('file',''), 'subtitle_forced': notify_params['subtitle_forced'],
'file_size': helpers.humanFileSize(media_part_info.get('file_size','')), 'subtitle_location': notify_params['subtitle_location'],
'indexes': media_part_info.get('indexes',''), 'subtitle_language': notify_params['subtitle_language'],
'section_id': metadata['section_id'], 'subtitle_language_code': notify_params['subtitle_language_code'],
'rating_key': metadata['rating_key'], 'file': notify_params['file'],
'parent_rating_key': metadata['parent_rating_key'], 'file_size': helpers.humanFileSize(notify_params['file_size']),
'grandparent_rating_key': metadata['grandparent_rating_key'], 'indexes': notify_params['indexes'],
'thumb': metadata['thumb'], 'section_id': notify_params['section_id'],
'parent_thumb': metadata['parent_thumb'], 'rating_key': notify_params['rating_key'],
'grandparent_thumb': metadata['grandparent_thumb'], 'parent_rating_key': notify_params['parent_rating_key'],
'poster_thumb': poster_thumb '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 return available_params
@@ -831,8 +841,8 @@ def build_server_notify_params(notify_action=None, **kwargs):
plex_tv = plextv.PlexTV() plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times() server_times = plex_tv.get_server_times()
pms_download_info = kwargs.pop('pms_download_info', {}) pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
plexpy_download_info = kwargs.pop('plexpy_download_info', {}) plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
if server_times: if server_times:
updated_at = server_times['updated_at'] updated_at = server_times['updated_at']
@@ -841,37 +851,38 @@ def build_server_notify_params(notify_action=None, **kwargs):
logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.") logger.error(u"Tautulli NotificationHandler :: Unable to retrieve server uptime.")
server_uptime = 'N/A' server_uptime = 'N/A'
available_params = {# Global paramaters available_params = {
'plexpy_version': common.VERSION_NUMBER, # Global paramaters
'plexpy_branch': plexpy.CONFIG.GIT_BRANCH, 'plexpy_version': common.VERSION_NUMBER,
'plexpy_commit': plexpy.CURRENT_VERSION, 'plexpy_branch': plexpy.CONFIG.GIT_BRANCH,
'server_name': server_name, 'plexpy_commit': plexpy.CURRENT_VERSION,
'server_uptime': server_uptime, 'server_name': server_name,
'server_version': server_times.get('version',''), 'server_uptime': server_uptime,
'action': notify_action.split('on_')[-1], 'server_version': server_times.get('version', ''),
'datestamp': arrow.now().format(date_format), 'action': notify_action.lstrip('on_'),
'timestamp': arrow.now().format(time_format), 'datestamp': arrow.now().format(date_format),
# Plex Media Server update parameters 'timestamp': arrow.now().format(time_format),
'update_version': pms_download_info.get('version',''), # Plex Media Server update parameters
'update_url': pms_download_info.get('download_url',''), 'update_version': pms_download_info['version'],
'update_release_date': arrow.get(pms_download_info.get('release_date','')).format(date_format) 'update_url': pms_download_info['download_url'],
if pms_download_info.get('release_date','') else '', 'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
'update_channel': 'Plex Pass' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public', if pms_download_info['release_date'] else '',
'update_platform': pms_download_info.get('platform',''), 'update_channel': 'Beta' if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plexpass' else 'Public',
'update_distro': pms_download_info.get('distro',''), 'update_platform': pms_download_info['platform'],
'update_distro_build': pms_download_info.get('build',''), 'update_distro': pms_download_info['distro'],
'update_requirements': pms_download_info.get('requirements',''), 'update_distro_build': pms_download_info['build'],
'update_extra_info': pms_download_info.get('extra_info',''), 'update_requirements': pms_download_info['requirements'],
'update_changelog_added': pms_download_info.get('changelog_added',''), 'update_extra_info': pms_download_info['extra_info'],
'update_changelog_fixed': pms_download_info.get('changelog_fixed',''), 'update_changelog_added': pms_download_info['changelog_added'],
# Tautulli update parameters 'update_changelog_fixed': pms_download_info['changelog_fixed'],
'plexpy_update_version': plexpy_download_info.get('tag_name', ''), # Tautulli update parameters
'plexpy_update_tar': plexpy_download_info.get('tarball_url', ''), 'plexpy_update_version': plexpy_download_info['tag_name'],
'plexpy_update_zip': plexpy_download_info.get('zipball_url', ''), 'plexpy_update_tar': plexpy_download_info['tarball_url'],
'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''), 'plexpy_update_zip': plexpy_download_info['zipball_url'],
'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''), 'plexpy_update_commit': kwargs.pop('plexpy_update_commit', ''),
'plexpy_update_changelog': plexpy_download_info.get('body', '') 'plexpy_update_behind': kwargs.pop('plexpy_update_behind', ''),
} 'plexpy_update_changelog': plexpy_download_info['body']
}
return available_params return available_params

View File

@@ -13,6 +13,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import time
import urllib import urllib
import plexpy import plexpy
@@ -519,7 +522,7 @@ class PmsConnect(object):
return output 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. Return processed and validated metadata list for requested item.
@@ -527,19 +530,33 @@ class PmsConnect(object):
Output: array 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 30 minutes ago
if int(time.time()) - _cache_time <= 1800:
return metadata
if rating_key: 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: 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: try:
xml_head = metadata.getElementsByTagName('MediaContainer') xml_head = metadata_xml.getElementsByTagName('MediaContainer')
except Exception as e: except Exception as e:
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata_details: %s." % e) logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata_details: %s." % e)
return {} return {}
metadata = {}
for a in xml_head: for a in xml_head:
if a.getAttribute('size'): if a.getAttribute('size'):
if a.getAttribute('size') != '1': if a.getAttribute('size') != '1':
@@ -1102,7 +1119,7 @@ class PmsConnect(object):
'subtitle_codec': helpers.get_xml_attr(stream, 'codec'), 'subtitle_codec': helpers.get_xml_attr(stream, 'codec'),
'subtitle_container': helpers.get_xml_attr(stream, 'container'), 'subtitle_container': helpers.get_xml_attr(stream, 'container'),
'subtitle_format': helpers.get_xml_attr(stream, 'format'), '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_location': 'external' if helpers.get_xml_attr(stream, 'key') else 'embedded',
'subtitle_language': helpers.get_xml_attr(stream, 'language'), 'subtitle_language': helpers.get_xml_attr(stream, 'language'),
'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode') 'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode')
@@ -1111,7 +1128,7 @@ class PmsConnect(object):
parts.append({'id': helpers.get_xml_attr(part, 'id'), parts.append({'id': helpers.get_xml_attr(part, 'id'),
'file': helpers.get_xml_attr(part, 'file'), 'file': helpers.get_xml_attr(part, 'file'),
'file_size': helpers.get_xml_attr(part, 'size'), '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 'streams': streams
}) })
@@ -1131,13 +1148,24 @@ class PmsConnect(object):
'audio_channels': audio_channels, 'audio_channels': audio_channels,
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels), 'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
'audio_profile': helpers.get_xml_attr(media, 'audioProfile'), '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 'parts': parts
}) })
metadata['media_info'] = medias metadata['media_info'] = medias
if metadata: if metadata:
metadata['_cache_time'] = int(time.time())
if cache_key:
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 return metadata
else: else:
return {} return {}
@@ -1299,6 +1327,7 @@ class PmsConnect(object):
# Get the source media type # Get the source media type
media_type = helpers.get_xml_attr(session, 'type') media_type = helpers.get_xml_attr(session, 'type')
rating_key = helpers.get_xml_attr(session, 'ratingKey') rating_key = helpers.get_xml_attr(session, 'ratingKey')
session_key = helpers.get_xml_attr(session, 'sessionKey')
# Get the user details # Get the user details
user_info = session.getElementsByTagName('User')[0] user_info = session.getElementsByTagName('User')[0]
@@ -1352,7 +1381,7 @@ class PmsConnect(object):
transcode_speed = helpers.get_xml_attr(transcode_info, 'speed') transcode_speed = helpers.get_xml_attr(transcode_info, 'speed')
transcode_details = {'transcode_key': helpers.get_xml_attr(transcode_info, 'key'), 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_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)), 'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'transcode_audio_channels': helpers.get_xml_attr(transcode_info, 'audioChannels'), 'transcode_audio_channels': helpers.get_xml_attr(transcode_info, 'audioChannels'),
@@ -1362,12 +1391,12 @@ class PmsConnect(object):
'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), # Blank but keep backwards compatibility 'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), # Blank but keep backwards compatibility
'transcode_container': helpers.get_xml_attr(transcode_info, 'container'), 'transcode_container': helpers.get_xml_attr(transcode_info, 'container'),
'transcode_protocol': helpers.get_xml_attr(transcode_info, 'protocol'), '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': helpers.get_xml_attr(transcode_info, 'transcodeHwDecoding'),
'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwDecodingTitle'), 'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwDecodingTitle'),
'transcode_hw_encode': helpers.get_xml_attr(transcode_info, 'transcodeHwEncoding'), 'transcode_hw_encode': helpers.get_xml_attr(transcode_info, 'transcodeHwEncoding'),
'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwEncodingTitle'), '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'), 'audio_decision': helpers.get_xml_attr(transcode_info, 'audioDecision'),
'video_decision': helpers.get_xml_attr(transcode_info, 'videoDecision'), 'video_decision': helpers.get_xml_attr(transcode_info, 'videoDecision'),
'subtitle_decision': helpers.get_xml_attr(transcode_info, 'subtitleDecision'), 'subtitle_decision': helpers.get_xml_attr(transcode_info, 'subtitleDecision'),
@@ -1397,6 +1426,10 @@ class PmsConnect(object):
'throttled': '0' # Keep for backwards compatibility '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 # Generate a combined transcode decision value
if transcode_details['video_decision'] == 'transcode' or transcode_details['audio_decision'] == 'transcode': if transcode_details['video_decision'] == 'transcode' or transcode_details['audio_decision'] == 'transcode':
transcode_decision = 'transcode' transcode_decision = 'transcode'
@@ -1489,7 +1522,7 @@ class PmsConnect(object):
subtitle_details = {'stream_subtitle_codec': helpers.get_xml_attr(subtitle_stream_info, 'codec'), 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_container': helpers.get_xml_attr(subtitle_stream_info, 'container'),
'stream_subtitle_format': helpers.get_xml_attr(subtitle_stream_info, 'format'), '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_location': helpers.get_xml_attr(subtitle_stream_info, 'location'),
'stream_subtitle_language': helpers.get_xml_attr(subtitle_stream_info, 'language'), 'stream_subtitle_language': helpers.get_xml_attr(subtitle_stream_info, 'language'),
'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info, 'languageCode'), 'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info, 'languageCode'),
@@ -1537,10 +1570,10 @@ class PmsConnect(object):
'stream_duration': helpers.get_xml_attr(stream_media_info, 'duration') or helpers.get_xml_attr(session, 'duration'), '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'), '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, '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'), 'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
'synced_version': 1 if sync_id else 0, 'synced_version': 1 if sync_id else 0,
'indexes': 1 if indexes == 'sd' else 0, 'indexes': int(indexes == 'sd'),
'bif_thumb': bif_thumb, 'bif_thumb': bif_thumb,
'subtitles': 1 if subtitle_id and subtitle_selected else 0 'subtitles': 1 if subtitle_id and subtitle_selected else 0
} }
@@ -1609,9 +1642,9 @@ class PmsConnect(object):
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id') part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
if sync_id: if sync_id:
metadata_details = self.get_metadata_details(sync_id=sync_id) metadata_details = self.get_metadata_details(sync_id=sync_id, cache_key=session_key)
else: 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 # Get the media info, fallback to first item if match id is not found
source_medias = metadata_details.pop('media_info', []) source_medias = metadata_details.pop('media_info', [])
@@ -1724,7 +1757,7 @@ class PmsConnect(object):
optimized_version_profile = '' optimized_version_profile = ''
# Entire session output (single dict for backwards compatibility) # 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, 'media_type': media_type,
'view_offset': view_offset, 'view_offset': view_offset,
'progress_percent': str(helpers.get_percent(view_offset, stream_details['stream_duration'])), 'progress_percent': str(helpers.get_percent(view_offset, stream_details['stream_duration'])),

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.7-beta" PLEXPY_RELEASE_VERSION = "v2.0.11-beta"

View File

@@ -96,7 +96,8 @@ def check_credentials(username, password, admin_login='0'):
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD): username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
return True, u'admin' 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' return True, u'admin'
elif not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS and user_login(username, password): elif not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS and user_login(username, password):
return True, u'guest' return True, u'guest'

View File

@@ -4437,7 +4437,6 @@ class WebInterface(object):
if session_key: if session_key:
return next((s for s in result['sessions'] if s['session_key'] == session_key), {}) return next((s for s in result['sessions'] if s['session_key'] == session_key), {})
counts = {'stream_count_direct_play': 0, counts = {'stream_count_direct_play': 0,
'stream_count_direct_stream': 0, 'stream_count_direct_stream': 0,
'stream_count_transcode': 0, 'stream_count_transcode': 0,