Compare commits

..

10 Commits

Author SHA1 Message Date
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
22 changed files with 10276 additions and 364 deletions

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## 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: Fix HW transcoding indicator on activity cards.
* Fix: 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

@@ -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,22 @@ 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 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')

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

@@ -135,6 +135,9 @@ AUDIO_QUALITY_PROFILES = {512: '512 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))
HW_DECODERS = ['dxva2', 'videotoolbox', 'mediacodecndk', 'vaapi']
HW_ENCODERS = ['qsv', 'nvenc', 'mf', 'videotoolbox', 'mediacodecndk', 'vaapi', 'nvenc']
SCHEDULER_LIST = ['Check GitHub for updates', SCHEDULER_LIST = ['Check GitHub for updates',
'Check for active sessions', 'Check for active sessions',
'Check for recently added items', 'Check for recently added items',
@@ -306,7 +309,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.'},

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,6 +947,8 @@ 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']

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
@@ -449,17 +449,16 @@ 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:
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 +475,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 +509,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 +616,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 +631,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 +837,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 +847,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.9-beta"

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,