Compare commits

..

22 Commits

Author SHA1 Message Date
JonnyWong16
4944ce1ca0 v2.1.20 2018-09-05 08:55:20 -07:00
JonnyWong16
f04873446a v2.1.20-beta 2018-09-02 18:06:45 -07:00
JonnyWong16
505b6b616e Add session_id parameter to get_activity API command 2018-09-02 11:27:34 -07:00
JonnyWong16
87dd43d699 Merge pull request #1305 from samwiseg00/fix/systemd_init
Change init script group value to be compatible with CentOS
2018-09-02 11:21:18 -07:00
samwiseg00
a48ebef9ae Change init script group value 2018-08-31 13:27:15 -04:00
JonnyWong16
e40483525b Redirect root to http_root 2018-08-27 21:41:11 -07:00
JonnyWong16
ebc563fd26 Remove unused pnotify css from login page 2018-08-27 21:39:40 -07:00
JonnyWong16
5bb3e189fe Update API docs 2018-08-27 21:27:09 -07:00
JonnyWong16
ed08df5224 Try getting missing parent_rating_key from parent_thumb first 2018-08-27 21:06:52 -07:00
JonnyWong16
ae2584b6f6 Helper function for splitting script args 2018-08-27 20:56:32 -07:00
JonnyWong16
f0e2355231 Log folder/file location on startup 2018-08-27 16:05:39 -07:00
JonnyWong16
878c48b491 Fix video and audio not showing on activity cards after refresh 2018-08-26 15:36:21 -07:00
JonnyWong16
ecfbb4de9b Fetch missing parent rating key when season is hidden 2018-08-24 22:02:21 -07:00
JonnyWong16
731af75c54 Merge pull request #1304 from samwiseg00/feature/add_env
Add TAUTULLI_PUBLIC_URL to environment variables
2018-08-24 08:07:21 -07:00
samwiseg00
9817da6012 Add TAUTULLI_PUBLIC_URL to environment variables 2018-08-24 02:59:34 -04:00
JonnyWong16
dd3f75f154 Add return_hash to pms_image_proxy API 2018-08-23 19:12:22 -07:00
JonnyWong16
1eee03fa8f Merge pull request #1303 from samwiseg00/feature/add_timestamp
Add UTC timestamp to notification params + OCD + Change discord timestamp function
2018-08-22 18:35:20 -07:00
samwiseg00
02af6c4e6c Change discord timestamp function to metadata params 2018-08-22 00:22:01 -04:00
samwiseg00
b8a9c4f5b7 Add UTC ISO time to notification handler 2018-08-22 00:21:49 -04:00
samwiseg00
0b227dc69e PEP8 functions in helpers 2018-08-22 00:18:54 -04:00
JonnyWong16
8228018dd0 Add sending notification log message 2018-08-20 14:49:00 -07:00
JonnyWong16
5c3086a049 Chnage get_notify_text_preview to POST 2018-08-19 15:08:27 -07:00
17 changed files with 152 additions and 55 deletions

27
API.md
View File

@@ -1,9 +1,15 @@
# API Reference # API Reference
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
## General structure ## General structure
The API endpoint is `http://ip:port + HTTP_ROOT + /api/v2?apikey=$apikey&cmd=$command` The API endpoint is
```
http://IP_ADDRESS:PORT + [/HTTP_ROOT] + /api/v2?apikey=$apikey&cmd=$command
```
Example:
```
http://localhost:8181/api/v2?apikey=66198313a092496b8a725867d2223b5f&cmd=get_metadata&rating_key=153037
```
Response example (default `json`) Response example (default `json`)
``` ```
@@ -354,7 +360,8 @@ Required parameters:
None None
Optional parameters: Optional parameters:
None session_key (int): Session key for the session info to return, OR
session_id (str): Session ID for the session info to return
Returns: Returns:
json: json:
@@ -1140,7 +1147,8 @@ Returns:
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_width": "1920" "video_width": "1920",
"selected": 0
}, },
{ {
"audio_bitrate": "384", "audio_bitrate": "384",
@@ -1153,7 +1161,8 @@ Returns:
"audio_profile": "", "audio_profile": "",
"audio_sample_rate": "48000", "audio_sample_rate": "48000",
"id": "511664", "id": "511664",
"type": "2" "type": "2",
"selected": 1
}, },
{ {
"id": "511953", "id": "511953",
@@ -1164,7 +1173,8 @@ Returns:
"subtitle_language": "English", "subtitle_language": "English",
"subtitle_language_code": "eng", "subtitle_language_code": "eng",
"subtitle_location": "external", "subtitle_location": "external",
"type": "3" "type": "3",
"selected": 1
} }
] ]
} }
@@ -2435,7 +2445,7 @@ Required parameters:
body (str): The body of the message body (str): The body of the message
Optional parameters: Optional parameters:
None script_args (str): The arguments for script notifications
Returns: Returns:
None None
@@ -2496,6 +2506,7 @@ Optional parameters:
img_format (str): png img_format (str): png
fallback (str): "poster", "cover", "art" fallback (str): "poster", "cover", "art"
refresh (bool): True or False whether to refresh the image cache refresh (bool): True or False whether to refresh the image cache
return_hash (bool): True or False to return the self-hosted image hash instead of the image
Returns: Returns:
None None

View File

@@ -1,5 +1,26 @@
# Changelog # Changelog
## v2.1.20 (2018-09-05)
* No changes.
## v2.1.20-beta (2018-09-02)
* Monitoring:
* Fix: Fetch messing season info when "Hide Seasons" is enabled for a show.
* Fix: Video and Audio details sometimes missing on activity cards.
* Notifications:
* New: Added UTC timestamp to notification parameters. (Thanks @samwiseg00)
* New: Added TAUTULLI_PUBLIC_URL to script environment variables. (Thanks @samwiseg00)
* UI:
* Change: Automatically redirect '/' to HTTP root if enabled.
* API:
* New: Added return_hash parameter to pms_image_proxy command.
* New: Added session_id parameter to get_activity command.
* Other:
* Change: Linux systemd startup script to use the "tautulli" group permission. (Thanks @samwiseg00)
## v2.1.19-beta (2018-08-19) ## v2.1.19-beta (2018-08-19)
* Notifications: * Notifications:

View File

@@ -439,7 +439,7 @@
$('#transcode_container-' + key).html(transcode_container); $('#transcode_container-' + key).html(transcode_container);
var video_decision = ''; var video_decision = '';
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.video_decision !== '') { if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) {
var v_res= ''; var v_res= '';
switch (s.video_resolution.toLowerCase()) { switch (s.video_resolution.toLowerCase()) {
case 'sd': case 'sd':
@@ -477,7 +477,7 @@
$('#video_decision-' + key).html(video_decision); $('#video_decision-' + key).html(video_decision);
var audio_decision = ''; var audio_decision = '';
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.audio_decision) { if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.stream_audio_decision) {
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase(); var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase(); var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
if (s.stream_audio_decision === 'transcode') { if (s.stream_audio_decision === 'transcode') {

View File

@@ -141,7 +141,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
$.ajax({ $.ajax({
url: url, url: url,
data: dataString, data: dataString,
type: 'post', type: 'POST',
beforeSend: function (jqXHR, settings) { beforeSend: function (jqXHR, settings) {
// Start loader etc. // Start loader etc.
feedback.prepend(loader); feedback.prepend(loader);

View File

@@ -12,7 +12,6 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet"> <link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet"> <link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet"> <link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">

View File

@@ -779,6 +779,7 @@
$.ajax({ $.ajax({
url: 'get_notify_text_preview', url: 'get_notify_text_preview',
type: 'POST',
data: { data: {
notify_action: action, notify_action: action,
subject: subject, subject: subject,

View File

@@ -25,7 +25,7 @@
# #
# - To create this user and give it ownership of the Tautulli directory: # - To create this user and give it ownership of the Tautulli directory:
# sudo adduser --system --no-create-home tautulli # sudo adduser --system --no-create-home tautulli
# sudo chown tautulli:nogroup -R /opt/Tautulli # sudo chown tautulli:tautulli -R /opt/Tautulli
# #
# - Adjust ExecStart= to point to: # - Adjust ExecStart= to point to:
# 1. Your Tautulli executable # 1. Your Tautulli executable
@@ -51,7 +51,7 @@ ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir
GuessMainPID=no GuessMainPID=no
Type=forking Type=forking
User=tautulli User=tautulli
Group=nogroup Group=tautlli
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -167,6 +167,15 @@ def initialize(config_file):
logger.info(u"Python {}".format( logger.info(u"Python {}".format(
sys.version sys.version
)) ))
logger.info(u"Program Dir: {}".format(
PROG_DIR
))
logger.info(u"Config File: {}".format(
CONFIG_FILE
))
logger.info(u"Database File: {}".format(
DB_FILE
))
if not CONFIG.BACKUP_DIR: if not CONFIG.BACKUP_DIR:
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups') CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')

View File

@@ -413,7 +413,7 @@ class API2:
body (str): The body of the message body (str): The body of the message
Optional parameters: Optional parameters:
None script_args (str): The arguments for script notifications
Returns: Returns:
None None
@@ -496,10 +496,16 @@ class API2:
""" Tries to make a API.md to simplify the api docs. """ """ Tries to make a API.md to simplify the api docs. """
head = '''# API Reference\n head = '''# API Reference\n
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
## General structure ## General structure
The API endpoint is `http://ip:port + HTTP_ROOT + /api/v2?apikey=$apikey&cmd=$command` The API endpoint is
```
http://IP_ADDRESS:PORT + [/HTTP_ROOT] + /api/v2?apikey=$apikey&cmd=$command
```
Example:
```
http://localhost:8181/api/v2?apikey=66198313a092496b8a725867d2223b5f&cmd=get_metadata&rating_key=153037
```
Response example (default `json`) Response example (default `json`)
``` ```
@@ -596,8 +602,9 @@ General optional parameters:
return return
elif self._api_cmd == 'pms_image_proxy': elif self._api_cmd == 'pms_image_proxy':
cherrypy.response.headers['Content-Type'] = 'image/jpeg' if 'return_hash' not in self._api_kwargs:
return out['response']['data'] cherrypy.response.headers['Content-Type'] = 'image/jpeg'
return out['response']['data']
if self._api_out_type == 'json': if self._api_out_type == 'json':
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8' cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'

View File

@@ -321,6 +321,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'}, {'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'},
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'}, {'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'},
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'}, {'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'},
{'name': 'UTC Time', 'type': 'int', 'value': 'utctime', 'description': 'The UTC timestamp in ISO format when the notification is triggered.'},
] ]
}, },
{ {

View File

@@ -33,6 +33,7 @@ import maxminddb
from operator import itemgetter from operator import itemgetter
import os import os
import re import re
import shlex
import socket import socket
import sys import sys
import time import time
@@ -202,17 +203,22 @@ def convert_seconds_to_minutes(s):
def today(): def today():
today = datetime.date.today() today = datetime.date.today()
yyyymmdd = datetime.date.isoformat(today) yyyymmdd = datetime.date.isoformat(today)
return yyyymmdd return yyyymmdd
def now(): def now():
now = datetime.datetime.now() now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S") return now.strftime("%Y-%m-%d %H:%M:%S")
def utc_now_iso(): def utc_now_iso():
utcnow = datetime.datetime.utcnow() utcnow = datetime.datetime.utcnow()
return utcnow.isoformat() return utcnow.isoformat()
def human_duration(s, sig='dhms'): def human_duration(s, sig='dhms'):
hd = '' hd = ''
@@ -1132,3 +1138,12 @@ def traverse_map(obj, func):
new_obj = func(obj) new_obj = func(obj)
return new_obj return new_obj
def split_args(args=None):
if isinstance(args, list):
return args
elif isinstance(args, basestring):
return [arg.decode(plexpy.SYS_ENCODING, 'ignore')
for arg in shlex.split(args.encode(plexpy.SYS_ENCODING, 'ignore'))]
return []

View File

@@ -23,7 +23,6 @@ import json
from operator import itemgetter from operator import itemgetter
import os import os
import re import re
import shlex
from string import Formatter from string import Formatter
import threading import threading
import time import time
@@ -337,12 +336,7 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
if notify_action in ('test', 'api'): if notify_action in ('test', 'api'):
subject = kwargs.pop('subject', 'Tautulli') subject = kwargs.pop('subject', 'Tautulli')
body = kwargs.pop('body', 'Test Notification') body = kwargs.pop('body', 'Test Notification')
script_args = kwargs.pop('script_args', []) script_args = helpers.split_args(kwargs.pop('script_args', []))
if script_args and isinstance(script_args, basestring):
# Attemps to format test script args for the user
script_args = [arg.decode(plexpy.SYS_ENCODING, 'ignore')
for arg in shlex.split(script_args.encode(plexpy.SYS_ENCODING, 'ignore'))]
else: else:
# Get the subject and body strings # Get the subject and body strings
@@ -749,6 +743,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'datestamp': now.format(date_format), 'datestamp': now.format(date_format),
'timestamp': now.format(time_format), 'timestamp': now.format(time_format),
'unixtime': int(time.time()), 'unixtime': int(time.time()),
'utctime': helpers.utc_now_iso(),
# Stream parameters # Stream parameters
'streams': stream_count, 'streams': stream_count,
'user_streams': user_stream_count, 'user_streams': user_stream_count,
@@ -969,6 +964,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
'datestamp': now.format(date_format), 'datestamp': now.format(date_format),
'timestamp': now.format(time_format), 'timestamp': now.format(time_format),
'unixtime': int(time.time()), 'unixtime': int(time.time()),
'utctime': helpers.utc_now_iso(),
# Plex Media Server update parameters # Plex Media Server update parameters
'update_version': pms_download_info['version'], 'update_version': pms_download_info['version'],
'update_url': pms_download_info['download_url'], 'update_url': pms_download_info['download_url'],
@@ -1048,8 +1044,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
if agent_id == 15: if agent_id == 15:
try: try:
script_args = [custom_formatter.format(arg.decode(plexpy.SYS_ENCODING, 'ignore'), **parameters) script_args = [custom_formatter.format(arg, **parameters) for arg in helpers.split_args(subject)]
for arg in shlex.split(subject.encode(plexpy.SYS_ENCODING, 'ignore'))]
except LookupError as e: except LookupError as e:
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e) logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
script_args = [] script_args = []
@@ -1254,7 +1249,8 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
def set_hash_image_info(img=None, rating_key=None, width=750, height=1000, def set_hash_image_info(img=None, rating_key=None, width=750, height=1000,
opacity=100, background='000000', blur=0, fallback=None): opacity=100, background='000000', blur=0, fallback=None,
add_to_db=True):
if not rating_key and not img: if not rating_key and not img:
return fallback return fallback
@@ -1272,18 +1268,19 @@ def set_hash_image_info(img=None, rating_key=None, width=750, height=1000,
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback) plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
img_hash = hashlib.sha256(img_string).hexdigest() img_hash = hashlib.sha256(img_string).hexdigest()
keys = {'img_hash': img_hash} if add_to_db:
values = {'img': img, keys = {'img_hash': img_hash}
'rating_key': rating_key, values = {'img': img,
'width': width, 'rating_key': rating_key,
'height': height, 'width': width,
'opacity': opacity, 'height': height,
'background': background, 'opacity': opacity,
'blur': blur, 'background': background,
'fallback': fallback} 'blur': blur,
'fallback': fallback}
db = database.MonitorDatabase() db = database.MonitorDatabase()
db.upsert('image_hash_lookup', key_dict=keys, value_dict=values) db.upsert('image_hash_lookup', key_dict=keys, value_dict=values)
return img_hash return img_hash

View File

@@ -795,6 +795,7 @@ class Notifier(object):
pass pass
def make_request(self, url, method='POST', **kwargs): def make_request(self, url, method='POST', **kwargs):
logger.info(u"Tautulli Notifiers :: Sending {name} notification...".format(name=self.NAME))
response, err_msg, req_msg = request.request_response2(url, method, **kwargs) response, err_msg, req_msg = request.request_response2(url, method, **kwargs)
if response and not err_msg: if response and not err_msg:
@@ -1145,7 +1146,7 @@ class DISCORD(Notifier):
# Build Discord post attachment # Build Discord post attachment
attachment = {'title': title, attachment = {'title': title,
'timestamp': helpers.utc_now_iso() 'timestamp': pretty_metadata.parameters['utctime']
} }
if self.config['color']: if self.config['color']:
@@ -3014,6 +3015,7 @@ class SCRIPTS(Notifier):
'PLEX_URL': plexpy.CONFIG.PMS_URL, 'PLEX_URL': plexpy.CONFIG.PMS_URL,
'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN, 'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN,
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'), 'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY, 'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING 'TAUTULLI_ENCODING': plexpy.SYS_ENCODING
}) })
@@ -3076,7 +3078,7 @@ class SCRIPTS(Notifier):
logger.error(u"Tautulli Notifiers :: No script folder specified.") logger.error(u"Tautulli Notifiers :: No script folder specified.")
return return
script_args = kwargs.get('script_args', []) script_args = helpers.split_args(kwargs.get('script_args', subject))
logger.debug(u"Tautulli Notifiers :: Trying to run notify script, action: %s, arguments: %s" logger.debug(u"Tautulli Notifiers :: Trying to run notify script, action: %s, arguments: %s"
% (action, script_args)) % (action, script_args))

View File

@@ -809,11 +809,27 @@ class PmsConnect(object):
elif metadata_type == 'episode': elif metadata_type == 'episode':
grandparent_rating_key = helpers.get_xml_attr(metadata_main, 'grandparentRatingKey') grandparent_rating_key = helpers.get_xml_attr(metadata_main, 'grandparentRatingKey')
show_details = self.get_metadata_details(grandparent_rating_key) show_details = self.get_metadata_details(grandparent_rating_key)
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
parent_media_index = helpers.get_xml_attr(metadata_main, 'parentIndex')
parent_thumb = helpers.get_xml_attr(metadata_main, 'parentThumb')
if not parent_rating_key:
# Try getting the parent_rating_key from the parent_thumb
if parent_thumb.startswith('/library/metadata/'):
parent_rating_key = parent_thumb.split('/')[3]
# Try getting the parent_rating_key from the grandparent's children
if not parent_rating_key:
children_list = self.get_item_children(grandparent_rating_key)
parent_rating_key = next((c['rating_key'] for c in children_list['children_list']
if c['media_index'] == parent_media_index), '')
metadata = {'media_type': metadata_type, metadata = {'media_type': metadata_type,
'section_id': section_id, 'section_id': section_id,
'library_name': library_name, 'library_name': library_name,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'), 'parent_rating_key': parent_rating_key,
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -821,7 +837,7 @@ class PmsConnect(object):
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'), 'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'), 'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'), 'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_media_index': parent_media_index,
'studio': show_details['studio'], 'studio': show_details['studio'],
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'), 'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'), 'summary': helpers.get_xml_attr(metadata_main, 'summary'),
@@ -834,7 +850,7 @@ class PmsConnect(object):
'duration': helpers.get_xml_attr(metadata_main, 'duration'), 'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'), 'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'), 'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'), 'parent_thumb': parent_thumb,
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'), 'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
'art': helpers.get_xml_attr(metadata_main, 'art'), 'art': helpers.get_xml_attr(metadata_main, 'art'),
'banner': show_details['banner'], 'banner': show_details['banner'],

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.1.19-beta" PLEXPY_RELEASE_VERSION = "v2.1.20"

View File

@@ -4031,7 +4031,7 @@ class WebInterface(object):
return self.real_pms_image_proxy(**kwargs) return self.real_pms_image_proxy(**kwargs)
@addtoapi('pms_image_proxy') @addtoapi('pms_image_proxy')
def real_pms_image_proxy(self, img='', rating_key=None, width=0, height=0, def real_pms_image_proxy(self, img=None, rating_key=None, width=750, height=1000,
opacity=100, background='000000', blur=0, img_format='png', opacity=100, background='000000', blur=0, img_format='png',
fallback=None, refresh=False, clip=False, **kwargs): fallback=None, refresh=False, clip=False, **kwargs):
""" Gets an image from the PMS and saves it to the image cache directory. """ Gets an image from the PMS and saves it to the image cache directory.
@@ -4051,6 +4051,7 @@ class WebInterface(object):
img_format (str): png img_format (str): png
fallback (str): "poster", "cover", "art" fallback (str): "poster", "cover", "art"
refresh (bool): True or False whether to refresh the image cache refresh (bool): True or False whether to refresh the image cache
return_hash (bool): True or False to return the self-hosted image hash instead of the image
Returns: Returns:
None None
@@ -4060,6 +4061,8 @@ class WebInterface(object):
logger.warn('No image input received.') logger.warn('No image input received.')
return return
return_hash = (kwargs.get('return_hash') == 'true')
if rating_key and not img: if rating_key and not img:
if fallback == 'art': if fallback == 'art':
img = '/library/metadata/{}/art'.format(rating_key) img = '/library/metadata/{}/art'.format(rating_key)
@@ -4070,9 +4073,13 @@ class WebInterface(object):
img = '/'.join(img_split[:5]) img = '/'.join(img_split[:5])
rating_key = rating_key or img_split[3] rating_key = rating_key or img_split[3]
img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format( img_hash = notification_handler.set_hash_image_info(
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback) img=img, rating_key=rating_key, width=width, height=height,
img_hash = hashlib.sha256(img_string).hexdigest() opacity=opacity, background=background, blur=blur, fallback=fallback,
add_to_db=return_hash)
if return_hash:
return {'img_hash': img_hash}
fp = '{}.{}'.format(img_hash, img_format) # we want to be able to preview the thumbs fp = '{}.{}'.format(img_hash, img_format) # we want to be able to preview the thumbs
c_dir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images') c_dir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images')
@@ -4899,7 +4906,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@addtoapi() @addtoapi()
def get_activity(self, session_key=None, **kwargs): def get_activity(self, session_key=None, session_id=None, **kwargs):
""" Get the current activity on the PMS. """ Get the current activity on the PMS.
``` ```
@@ -4907,7 +4914,8 @@ class WebInterface(object):
None None
Optional parameters: Optional parameters:
None session_key (int): Session key for the session info to return, OR
session_id (str): Session ID for the session info to return
Returns: Returns:
json: json:
@@ -5138,6 +5146,8 @@ class WebInterface(object):
if result: if result:
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), {})
if session_id:
return next((s for s in result['sessions'] if s['session_id'] == session_id), {})
counts = {'stream_count_direct_play': 0, counts = {'stream_count_direct_play': 0,
'stream_count_direct_stream': 0, 'stream_count_direct_stream': 0,

View File

@@ -202,6 +202,8 @@ def initialize(options):
# Prevent time-outs # Prevent time-outs
cherrypy.engine.timeout_monitor.unsubscribe() cherrypy.engine.timeout_monitor.unsubscribe()
cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf) cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf)
if plexpy.HTTP_ROOT != '/':
cherrypy.tree.mount(BaseRedirect(), '/')
try: try:
logger.info(u"Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol, logger.info(u"Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol,
@@ -218,3 +220,9 @@ def initialize(options):
sys.exit(1) sys.exit(1)
cherrypy.server.wait() cherrypy.server.wait()
class BaseRedirect(object):
@cherrypy.expose
def index(self):
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)