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
## 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)
* Monitoring:

View File

@@ -844,6 +844,18 @@ a .users-poster-face:hover {
-webkit-flex-grow: 1;
flex-grow: 1;
}
.dashboard-activity-info-item .sub-value.platform-right {
margin-right: 55px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dashboard-activity-info-item .sub-value.time-right {
margin-right: 60px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dashboard-activity-info-item .sub-value .ip-container {
display: inline-flex;
}
@@ -1261,7 +1273,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
.dashboard-stats-info {
width: 100%;
font-size: 12px;
padding: 3px 0 5px 15px;
padding: 3px 0 0 15px;
position: relative;
}
.dashboard-stats-info-list {

View File

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

View File

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

2
lib/idna/__init__.py Normal file
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_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,'
'transcode_width INTEGER, transcode_height INTEGER, '
'transcode_hw_decoding INTEGER, transcode_hw_encoding INTEGER, '
'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, '
'synced_version INTEGER, synced_version_profile TEXT, '
'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, write_attempts INTEGER DEFAULT 0, '
@@ -468,8 +469,9 @@ def dbcheck():
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
'transcode_hw_requested INTEGER, transcode_hw_full_pipeline INTEGER, transcode_hw_decode TEXT, '
'transcode_hw_decode_title TEXT, transcode_hw_encode TEXT, transcode_hw_encode_title TEXT, '
'transcode_hw_requested INTEGER, transcode_hw_full_pipeline INTEGER, '
'transcode_hw_decode TEXT, transcode_hw_decode_title TEXT, transcode_hw_decoding INTEGER, '
'transcode_hw_encode TEXT, transcode_hw_encode_title TEXT, transcode_hw_encoding INTEGER, '
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
@@ -917,6 +919,18 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN optimized_version_title TEXT'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT transcode_hw_decoding FROM sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN transcode_hw_decoding INTEGER'
)
c_db.execute(
'ALTER TABLE sessions ADD COLUMN transcode_hw_encoding INTEGER'
)
# Upgrade session_history table from earlier versions
try:
c_db.execute('SELECT reference_id FROM session_history')
@@ -1159,6 +1173,22 @@ def dbcheck():
'ALTER TABLE session_history_media_info ADD COLUMN optimized_version_title TEXT '
)
# Upgrade session_history_media_info table from earlier versions
try:
c_db.execute('SELECT transcode_hw_decoding FROM session_history_media_info')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table session_history_media_info.")
c_db.execute(
'ALTER TABLE session_history_media_info ADD COLUMN transcode_hw_decoding INTEGER '
)
c_db.execute(
'ALTER TABLE session_history_media_info ADD COLUMN transcode_hw_encoding INTEGER '
)
c_db.execute(
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
)
# Upgrade users table from earlier versions
try:
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/>.
import datetime
import threading
import os
import time
from apscheduler.schedulers.background import BackgroundScheduler
@@ -26,7 +26,6 @@ import datafactory
import helpers
import logger
import notification_handler
import notifiers
import pmsconnect
@@ -75,9 +74,12 @@ class ActivityHandler(object):
monitor_proc.write_session(session=session, notify=False)
def on_start(self):
if self.is_valid_session() and self.get_live_session():
if self.is_valid_session():
session = self.get_live_session()
if not session:
return
# Some DLNA clients create a new session temporarily when browsing the library
# Wait and get session again to make sure it is an actual session
if session['platform'] == 'DLNA':
@@ -124,6 +126,7 @@ class ActivityHandler(object):
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
% (str(self.get_session_key()), str(self.get_rating_key())))
ap.delete_session(session_key=self.get_session_key())
delete_metadata_cache(self.get_session_key())
def on_pause(self, still_paused=False):
if self.is_valid_session():
@@ -436,12 +439,12 @@ def force_stop_stream(session_key):
ap.delete_session(session_key=session_key)
else:
sessions['write_attempts'] += 1
session['write_attempts'] += 1
if sessions['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
if session['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
logger.warn(u"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
"Will try again in 30 seconds. Write attempt %s."
% (sessions['session_key'], sessions['rating_key'], str(sessions['write_attempts'])))
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
ap.increment_write_attempts(session_key=session_key)
# Reschedule for 30 seconds later
@@ -449,12 +452,13 @@ def force_stop_stream(session_key):
args=[session_key], seconds=30)
else:
logger.warn(u"Tautulli Monitor :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
logger.warn(u"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
"Removing session from the database. Write attempt %s."
% (sessions['session_key'], sessions['rating_key'], str(sessions['write_attempts'])))
logger.info(u"Tautulli Monitor :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (sessions['session_key'], sessions['rating_key']))
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (session['session_key'], session['rating_key']))
ap.delete_session(session_key=session_key)
delete_metadata_cache(session_key)
def clear_recently_added_queue(rating_key):
@@ -519,3 +523,11 @@ def on_created(rating_key, **kwargs):
else:
logger.error(u"Tautulli TimelineHandler :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
def delete_metadata_cache(session_key):
try:
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % session_key))
except IOError as e:
logger.error(u"Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
% (session_key, e))

View File

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

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))
HW_DECODERS = ['dxva2', 'videotoolbox', 'mediacodecndk', 'vaapi']
HW_ENCODERS = ['qsv', 'nvenc', 'mf', 'videotoolbox', 'mediacodecndk', 'vaapi', 'nvenc']
SCHEDULER_LIST = ['Check GitHub for updates',
'Check for active sessions',
'Check for recently added items',
@@ -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 Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec', 'description': 'The audio codec of the transcoded stream.'},
{'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels', 'description': 'The audio channels of the transcoded stream.'},
{'name': 'Transcode Hardware', 'type': 'int', 'value': 'transcode_hardware', 'description': 'If hardware transcoding is used.', 'example': '0 or 1'},
{'name': 'Transcode HW Requested', 'type': 'int', 'value': 'transcode_hw_requested', 'description': 'If hardware decoding/encoding was requested.', 'example': '0 or 1'},
{'name': 'Transcode HW Decoding', 'type': 'int', 'value': 'transcode_hw_decoding', 'description': 'If hardware decoding is used.', 'example': '0 or 1'},
{'name': 'Transcode HW Decoding Codec', 'type': 'str', 'value': 'transcode_hw_decode', 'description': 'The hardware decoding codec.'},
{'name': 'Transcode HW Decoding Title', 'type': 'str', 'value': 'transcode_hw_decode_title', 'description': 'The hardware decoding codec title.'},
{'name': 'Transcode HW Encoding', 'type': 'int', 'value': 'transcode_hw_encoding', 'description': 'If hardware encoding is used.', 'example': '0 or 1'},
{'name': 'Transcode HW Encoding Codec', 'type': 'str', 'value': 'transcode_hw_encode', 'description': 'The hardware encoding codec.'},
{'name': 'Transcode HW Encoding Title', 'type': 'str', 'value': 'transcode_hw_encode_title', 'description': 'The hardware encoding codec title.'},
{'name': 'Session Key', 'type': 'str', 'value': 'session_key', 'description': 'The unique identifier for the session.'},
{'name': 'Transcode Key', 'type': 'str', 'value': 'transcode_key', 'description': 'The unique identifier for the transcode session.'},
{'name': 'Session ID', 'type': 'str', 'value': 'session_id', 'description': 'The unique identifier for the stream.'},

View File

@@ -882,6 +882,7 @@ class DataFactory(object):
'stream_video_framerate, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
'session_history_metadata.media_type, title, grandparent_title ' \
'FROM session_history_media_info ' \
'JOIN session_history ON session_history_media_info.id = session_history.id ' \
@@ -899,6 +900,7 @@ class DataFactory(object):
'stream_video_framerate, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
'media_type, title, grandparent_title ' \
'FROM sessions ' \
'WHERE session_key = ? %s' % user_cond
@@ -945,6 +947,8 @@ class DataFactory(object):
'subtitles': item['subtitles'],
'stream_subtitle_decision': item['stream_subtitle_decision'],
'stream_subtitle_codec': item['stream_subtitle_codec'],
'transcode_hw_decoding': item['transcode_hw_decoding'],
'transcode_hw_encoding': item['transcode_hw_encoding'],
'media_type': item['media_type'],
'title': item['title'],
'grandparent_title': item['grandparent_title']

View File

@@ -154,7 +154,7 @@ class HTTPHandler(object):
try:
if self.output_format == 'text':
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)
elif self.output_format == 'json':
output = helpers.convert_xml_to_json(response_content)

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
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:
return next((s for s in result['sessions'] if s['session_key'] == session_key), {})
counts = {'stream_count_direct_play': 0,
'stream_count_direct_stream': 0,
'stream_count_transcode': 0,