More rename, more -python2

This commit is contained in:
2021-02-05 17:29:23 +01:00
parent b867dc9be2
commit 1df28243c3
40 changed files with 1424 additions and 1412 deletions

View File

@@ -21,9 +21,6 @@ import sys
import threading
import uuid
import future.moves.queue as queue
from future.builtins import range
# Some cut down versions of Python may not include this module and it's not critical for us
try:
import webbrowser
@@ -528,19 +525,6 @@ def start():
# Cancel processing exports
exporter.cancel_exports()
if CONFIG.SYSTEM_ANALYTICS:
global TRACKER
TRACKER = initialize_tracker()
# Send system analytics events
if not CONFIG.FIRST_RUN_COMPLETE:
analytics_event(category='system', action='install')
elif _UPDATE:
analytics_event(category='system', action='update')
analytics_event(category='system', action='start')
_STARTED = True

View File

@@ -13,33 +13,21 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import object
import datetime
import os
import time
from apscheduler.triggers.date import DateTrigger
import pytz
from apscheduler.triggers.date import DateTrigger
import jellypy
if jellypy.PYTHON2:
import activity_processor
import datafactory
import helpers
import logger
import notification_handler
import pmsconnect
else:
from jellypy import activity_processor
from jellypy import datafactory
from jellypy import helpers
from jellypy import logger
from jellypy import notification_handler
from jellypy import pmsconnect
from jellypy import activity_processor
from jellypy import datafactory
from jellypy import helpers
from jellypy import logger
from jellypy import notification_handler
from jellypy import pmsconnect
ACTIVITY_SCHED = None
@@ -134,7 +122,7 @@ class ActivityHandler(object):
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
# Send notification after updating db
#jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
# jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
# Write the new session to our temp session table
self.update_db_session(session=session, notify=True)
@@ -218,7 +206,8 @@ class ActivityHandler(object):
def on_change(self):
if self.is_valid_session():
logger.debug("Tautulli ActivityHandler :: Session %s has changed transcode decision." % str(self.get_session_key()))
logger.debug(
"Tautulli ActivityHandler :: Session %s has changed transcode decision." % str(self.get_session_key()))
# Update the session state and viewOffset
self.update_db_session()
@@ -316,8 +305,8 @@ class ActivityHandler(object):
# Make sure the same item is being played
if (this_rating_key == last_rating_key
or this_rating_key == last_rating_key_websocket
or this_live_uuid == last_live_uuid) \
or this_rating_key == last_rating_key_websocket
or this_live_uuid == last_live_uuid) \
and this_guid == last_guid:
# Update the session state and viewOffset
if this_state == 'playing':
@@ -374,8 +363,8 @@ class ActivityHandler(object):
for d in watched_notifiers:
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
'notifier_id': d['notifier_id'],
'notify_action': 'on_watched'})
'notifier_id': d['notifier_id'],
'notify_action': 'on_watched'})
else:
# We don't have this session in our table yet, start a new one.
@@ -439,9 +428,9 @@ class TimelineHandler(object):
# Add a new media item to the recently added queue
if media_type and section_id > 0 and \
((state_type == 0 and metadata_state == 'created')): # or \
#(jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \
#media_state == 'analyzing' and queue_size is None)):
((state_type == 0 and metadata_state == 'created')): # or \
# (jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \
# media_state == 'analyzing' and queue_size is None)):
if media_type in ('episode', 'track'):
metadata = self.get_metadata()
@@ -460,8 +449,9 @@ class TimelineHandler(object):
RECENTLY_ADDED_QUEUE[rating_key] = set([grandparent_rating_key])
logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s, grandparent %s) added to recently added queue."
% (title, str(rating_key), str(grandparent_rating_key)))
logger.debug(
"Tautulli TimelineHandler :: Library item '%s' (%s, grandparent %s) added to recently added queue."
% (title, str(rating_key), str(grandparent_rating_key)))
# Schedule a callback to clear the recently added queue
schedule_callback('rating_key-{}'.format(grandparent_rating_key),
@@ -479,8 +469,9 @@ class TimelineHandler(object):
parent_set.add(rating_key)
RECENTLY_ADDED_QUEUE[parent_rating_key] = parent_set
logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s , parent %s) added to recently added queue."
% (title, str(rating_key), str(parent_rating_key)))
logger.debug(
"Tautulli TimelineHandler :: Library item '%s' (%s , parent %s) added to recently added queue."
% (title, str(rating_key), str(parent_rating_key)))
# Schedule a callback to clear the recently added queue
schedule_callback('rating_key-{}'.format(parent_rating_key),
@@ -503,8 +494,8 @@ class TimelineHandler(object):
# A movie, show, or artist is done processing
elif media_type in ('movie', 'show', 'artist') and section_id > 0 and \
state_type == 5 and metadata_state is None and queue_size is None and \
rating_key in RECENTLY_ADDED_QUEUE:
state_type == 5 and metadata_state is None and queue_size is None and \
rating_key in RECENTLY_ADDED_QUEUE:
logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s) done processing metadata."
% (title, str(rating_key)))
@@ -618,8 +609,9 @@ def force_stop_stream(session_key, title, user):
if row_id:
# If session is written to the database successfully, remove the session from the session table
logger.info("Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (session['session_key'], session['rating_key']))
logger.info(
"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (session['session_key'], session['rating_key']))
ap.delete_session(row_id=row_id)
delete_metadata_cache(session_key)
@@ -627,9 +619,10 @@ def force_stop_stream(session_key, title, user):
session['write_attempts'] += 1
if session['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
"Will try again in 30 seconds. Write attempt %s."
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
logger.warn(
"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
"Will try again in 30 seconds. Write attempt %s."
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
ap.increment_write_attempts(session_key=session_key)
# Reschedule for 30 seconds later
@@ -637,11 +630,13 @@ def force_stop_stream(session_key, title, user):
args=[session_key, session['full_title'], session['user']], seconds=30)
else:
logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
"Removing session from the database. Write attempt %s."
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
logger.info("Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
% (session['session_key'], session['rating_key']))
logger.warn(
"Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
"Removing session from the database. Write attempt %s."
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
logger.info(
"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)

View File

@@ -13,35 +13,19 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
import threading
import jellypy
if jellypy.PYTHON2:
import activity_handler
import activity_processor
import database
import helpers
import libraries
import logger
import notification_handler
import plextv
import pmsconnect
import web_socket
else:
from jellypy import activity_handler
from jellypy import activity_processor
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import notification_handler
from jellypy import plextv
from jellypy import pmsconnect
from jellypy import web_socket
from jellypy import activity_handler
from jellypy import activity_processor
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import notification_handler
from jellypy import plextv
from jellypy import pmsconnect
from jellypy import web_socket
monitor_lock = threading.Lock()
ext_ping_count = 0
@@ -50,7 +34,6 @@ int_ping_count = 0
def check_active_sessions(ws_request=False):
with monitor_lock:
monitor_db = database.MonitorDatabase()
monitor_process = activity_processor.ActivityProcessor()
@@ -82,17 +65,21 @@ def check_active_sessions(ws_request=False):
if session['state'] == 'paused':
logger.debug("Tautulli Monitor :: Session %s paused." % stream['session_key'])
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
jellypy.NOTIFY_QUEUE.put(
{'stream_data': stream.copy(), 'notify_action': 'on_pause'})
if session['state'] == 'playing' and stream['state'] == 'paused':
logger.debug("Tautulli Monitor :: Session %s resumed." % stream['session_key'])
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
jellypy.NOTIFY_QUEUE.put(
{'stream_data': stream.copy(), 'notify_action': 'on_resume'})
if session['state'] == 'error':
logger.debug("Tautulli Monitor :: Session %s encountered an error." % stream['session_key'])
logger.debug(
"Tautulli Monitor :: Session %s encountered an error." % stream['session_key'])
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_error'})
jellypy.NOTIFY_QUEUE.put(
{'stream_data': stream.copy(), 'notify_action': 'on_error'})
if stream['state'] == 'paused' and not ws_request:
# The stream is still paused so we need to increment the paused_counter
@@ -130,26 +117,30 @@ def check_active_sessions(ws_request=False):
'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
jellypy.NOTIFY_QUEUE.put(
{'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
else:
# Subsequent buffer notifications after wait time
if helpers.timestamp() > buffer_values[0]['buffer_last_triggered'] + \
jellypy.CONFIG.BUFFER_WAIT:
logger.info("Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
% stream['user'])
logger.info(
"Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
% stream['user'])
# Set the buffer trigger time
monitor_db.action('UPDATE sessions '
'SET buffer_last_triggered = strftime("%s","now") '
'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
jellypy.NOTIFY_QUEUE.put(
{'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
logger.debug("Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
% (stream['session_key'],
buffer_values[0]['buffer_count'],
buffer_values[0]['buffer_last_triggered']))
logger.debug(
"Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
% (stream['session_key'],
buffer_values[0]['buffer_count'],
buffer_values[0]['buffer_last_triggered']))
# Check if the user has reached the offset in the media we defined as the "watched" percent
# Don't trigger if state is buffer as some clients push the progress to the end when
@@ -157,11 +148,15 @@ def check_active_sessions(ws_request=False):
if session['state'] != 'buffering':
progress_percent = helpers.get_percent(session['view_offset'], session['duration'])
notify_states = notification_handler.get_notify_state(session=session)
if (session['media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
session['media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
session['media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
if (session[
'media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
session[
'media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
session[
'media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
jellypy.NOTIFY_QUEUE.put(
{'stream_data': stream.copy(), 'notify_action': 'on_watched'})
else:
# The user has stopped playing a stream
@@ -173,14 +168,18 @@ def check_active_sessions(ws_request=False):
stream['stopped'] = helpers.timestamp()
monitor_db.action('UPDATE sessions SET stopped = ?, state = ? '
'WHERE session_key = ? AND rating_key = ?',
[stream['stopped'], 'stopped', stream['session_key'], stream['rating_key']])
[stream['stopped'], 'stopped', stream['session_key'],
stream['rating_key']])
progress_percent = helpers.get_percent(stream['view_offset'], stream['duration'])
notify_states = notification_handler.get_notify_state(session=stream)
if (stream['media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
stream['media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
stream['media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
if (stream[
'media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
stream[
'media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
stream[
'media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
@@ -197,14 +196,16 @@ def check_active_sessions(ws_request=False):
stream['write_attempts'] += 1
if stream['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
"Will try again on the next pass. Write attempt %s."
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
logger.warn(
"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
"Will try again on the next pass. Write attempt %s."
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
monitor_process.increment_write_attempts(session_key=stream['session_key'])
else:
logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
"Removing session from the database. Write attempt %s."
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
logger.warn(
"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
"Removing session from the database. Write attempt %s."
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
logger.debug("Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
% (stream['session_key'], stream['rating_key']))
monitor_process.delete_session(session_key=stream['session_key'])
@@ -216,7 +217,8 @@ def check_active_sessions(ws_request=False):
if new_session:
logger.debug("Tautulli Monitor :: Session %s started by user %s (%s) with ratingKey %s (%s)%s."
% (str(session['session_key']), str(session['user_id']), session['username'],
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
str(session['rating_key']), session['full_title'],
'[Live TV]' if session['live'] else ''))
else:
logger.debug("Tautulli Monitor :: Unable to read session list.")
@@ -256,7 +258,6 @@ def connect_server(log=True, startup=False):
def check_server_updates():
with monitor_lock:
logger.info("Tautulli Monitor :: Checking for PMS updates...")

View File

@@ -13,28 +13,17 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import object
from collections import defaultdict
import json
from collections import defaultdict
import jellypy
if jellypy.PYTHON2:
import database
import helpers
import libraries
import logger
import pmsconnect
import users
else:
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import pmsconnect
from jellypy import users
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import pmsconnect
from jellypy import users
class ActivityProcessor(object):
@@ -229,7 +218,8 @@ class ActivityProcessor(object):
(session['session_key'], session['rating_key'], session['media_type']))
return session['id']
real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(session['paused_counter'])
real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(
session['paused_counter'])
if not is_import and jellypy.CONFIG.LOGGING_IGNORE_INTERVAL:
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
@@ -249,22 +239,27 @@ class ActivityProcessor(object):
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
(real_play_time < int(import_ignore_interval)):
logging_enabled = False
logger.debug("Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
"seconds, so we're not logging it." %
(session['rating_key'], str(real_play_time), import_ignore_interval))
logger.debug(
"Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
"seconds, so we're not logging it." %
(session['rating_key'], str(real_play_time), import_ignore_interval))
if not is_import and not user_details['keep_history']:
logging_enabled = False
logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details['username'])
logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details[
'username'])
elif not is_import and not library_details['keep_history']:
logging_enabled = False
logger.debug("Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details['section_name'])
logger.debug(
"Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details[
'section_name'])
if logging_enabled:
# Fetch metadata first so we can return false if it fails
if not is_import:
logger.debug("Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
logger.debug(
"Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
pms_connect = pmsconnect.PmsConnect()
if session['live']:
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']),
@@ -383,7 +378,7 @@ class ActivityProcessor(object):
args = [new_session['id'], new_session['id']]
self.db.action(query=query, args=args)
# logger.debug("Tautulli ActivityProcessor :: Successfully written history item, last id for session_history is %s"
# % last_id)

View File

@@ -15,14 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import object
from hashing_passwords import check_hash
from io import open
import hashlib
import inspect
import json
@@ -31,39 +23,26 @@ import random
import re
import time
import traceback
from io import open
import cherrypy
import xmltodict
from hashing_passwords import check_hash
import jellypy
if jellypy.PYTHON2:
import common
import config
import database
import helpers
import libraries
import logger
import mobile_app
import notification_handler
import notifiers
import newsletter_handler
import newsletters
import plextv
import users
else:
from jellypy import common
from jellypy import config
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import mobile_app
from jellypy import notification_handler
from jellypy import notifiers
from jellypy import newsletter_handler
from jellypy import newsletters
from jellypy import plextv
from jellypy import users
from jellypy import common
from jellypy import config
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import mobile_app
from jellypy import notification_handler
from jellypy import notifiers
from jellypy import newsletter_handler
from jellypy import newsletters
from jellypy import plextv
from jellypy import users
class API2(object):
@@ -91,7 +70,8 @@ class API2(object):
if md is True:
docs[f] = inspect.getdoc(getattr(self, f)) if inspect.getdoc(getattr(self, f)) else None
else:
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(getattr(self, f)) else None
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(
getattr(self, f)) else None
return docs
def docs_md(self):
@@ -128,7 +108,8 @@ class API2(object):
self._api_response_code = 400
elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods:
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods)))
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (
kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods)))
self._api_response_code = 400
self._api_callback = kwargs.pop('callback', None)
@@ -232,7 +213,6 @@ class API2(object):
continue
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
d = {
'time': temp_loglevel_and_time[0],
'loglevel': loglvl,
@@ -778,7 +758,8 @@ General optional parameters:
result = call(**self._api_kwargs)
except Exception as e:
logger.api_error('Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
logger.api_error(
'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
self._api_response_code = 500
if self._api_debug:
cherrypy.request.show_tracebacks = True

View File

@@ -19,16 +19,7 @@
## Stolen from Sick-Beard's classes.py ##
#########################################
from __future__ import unicode_literals
from future.moves.urllib.request import FancyURLopener
import jellypy
if jellypy.PYTHON2:
from common import USER_AGENT
else:
from jellypy.common import USER_AGENT
from jellypy.common import USER_AGENT
class PlexPyURLopener(FancyURLopener):

View File

@@ -15,21 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import distro
import platform
from collections import OrderedDict
import jellypy
if jellypy.PYTHON2:
import version
else:
from jellypy import version
import distro
from jellypy import version
# Identify Our Application
PRODUCT = 'Tautulli'
PRODUCT = 'JellPy'
PLATFORM = platform.system()
PLATFORM_RELEASE = platform.release()
PLATFORM_VERSION = platform.version()
@@ -243,7 +238,7 @@ DATE_TIME_FORMATS = [
{'value': 'YYYY', 'description': 'Numeric, four digits', 'example': '1999, 2003'},
{'value': 'YY', 'description': 'Numeric, two digits', 'example': '99, 03'}
]
},
},
{
'category': 'Month',
'parameters': [
@@ -253,7 +248,7 @@ DATE_TIME_FORMATS = [
{'value': 'M', 'description': 'Numeric, without leading zeros', 'example': '1-12'},
{'value': 'Mo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 12th'},
]
},
},
{
'category': 'Day of the Year',
'parameters': [
@@ -261,7 +256,7 @@ DATE_TIME_FORMATS = [
{'value': 'DDD', 'description': 'Numeric, without leading zeros', 'example': '1-365'},
{'value': 'DDDo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd, ... 365th'},
]
},
},
{
'category': 'Day of the Month',
'parameters': [
@@ -269,7 +264,7 @@ DATE_TIME_FORMATS = [
{'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '1-31'},
{'value': 'Do', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 31st'},
]
},
},
{
'category': 'Day of the Week',
'parameters': [
@@ -279,7 +274,7 @@ DATE_TIME_FORMATS = [
{'value': 'd', 'description': 'Numeric', 'example': '0-6'},
{'value': 'do', 'description': 'Numeric, with suffix', 'example': '0th, 1st ... 6th'},
]
},
},
{
'category': 'Hour',
'parameters': [
@@ -288,361 +283,620 @@ DATE_TIME_FORMATS = [
{'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '01-12'},
{'value': 'h', 'description': '12-hour, without leading zeros', 'example': '1-12'},
]
},
},
{
'category': 'Minute',
'parameters': [
{'value': 'mm', 'description': 'Numeric, with leading zeros', 'example': '00-59'},
{'value': 'm', 'description': 'Numeric, without leading zeros', 'example': '0-59'},
]
},
},
{
'category': 'Second',
'parameters': [
{'value': 'ss', 'description': 'Numeric, with leading zeros', 'example': '00-59'},
{'value': 's', 'description': 'Numeric, without leading zeros', 'example': '0-59'},
]
},
},
{
'category': 'AM / PM',
'parameters': [
{'value': 'A', 'description': 'AM/PM uppercase', 'example': 'AM, PM'},
{'value': 'a', 'description': 'am/pm lowercase', 'example': 'am, pm'},
]
},
},
{
'category': 'Timezone',
'parameters': [
{'value': 'ZZ', 'description': 'UTC offset', 'example': '+0100, -0700'},
{'value': 'Z', 'description': 'UTC offset', 'example': '+01:00, -07:00'},
]
},
},
{
'category': 'Timestamp',
'parameters': [
{'value': 'X', 'description': 'Unix timestamp', 'example': 'E.g. 1456887825'},
]
},
},
]
NOTIFICATION_PARAMETERS = [
{
'category': 'Global',
'parameters': [
{'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', 'description': 'The current version of Tautulli.'},
{'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', 'description': 'The current git remote of Tautulli.'},
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', 'description': 'The current git branch of Tautulli.'},
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', 'description': 'The current git commit hash of Tautulli.'},
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
{'name': 'Server IP', 'type': 'str', 'value': 'server_ip', 'description': 'The connection IP address for your Plex Server.'},
{'name': 'Server Port', 'type': 'int', 'value': 'server_port', 'description': 'The connection port for your Plex Server.'},
{'name': 'Server URL', 'type': 'str', 'value': 'server_url', 'description': 'The connection URL for your Plex Server.'},
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', 'description': 'The platform of your Plex Server.'},
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
{'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year when the notification is triggered.'},
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month when the notification is triggered.', 'example': '1 to 12'},
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day when the notification is triggered.', 'example': '1 to 31'},
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour when the notification is triggered.', 'example': '0 to 23'},
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute when the notification is triggered.', 'example': '0 to 59'},
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second when the notification is triggered.', 'example': '0 to 59'},
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday when the notification is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number when the notification is triggered.', 'example': '1 to 52'},
{'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': '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.'},
{'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version',
'description': 'The current version of Tautulli.'},
{'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote',
'description': 'The current git remote of Tautulli.'},
{'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch',
'description': 'The current git branch of Tautulli.'},
{'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit',
'description': 'The current git commit hash of Tautulli.'},
{'name': 'Server Name', 'type': 'str', 'value': 'server_name',
'description': 'The name of your Plex Server.'},
{'name': 'Server IP', 'type': 'str', 'value': 'server_ip',
'description': 'The connection IP address for your Plex Server.'},
{'name': 'Server Port', 'type': 'int', 'value': 'server_port',
'description': 'The connection port for your Plex Server.'},
{'name': 'Server URL', 'type': 'str', 'value': 'server_url',
'description': 'The connection URL for your Plex Server.'},
{'name': 'Server Platform', 'type': 'str', 'value': 'server_platform',
'description': 'The platform of your Plex Server.'},
{'name': 'Server Version', 'type': 'str', 'value': 'server_version',
'description': 'The current version of your Plex Server.'},
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id',
'description': 'The unique identifier for your Plex Server.'},
{'name': 'Action', 'type': 'str', 'value': 'action',
'description': 'The action that triggered the notification.'},
{'name': 'Current Year', 'type': 'int', 'value': 'current_year',
'description': 'The year when the notification is triggered.'},
{'name': 'Current Month', 'type': 'int', 'value': 'current_month',
'description': 'The month when the notification is triggered.', 'example': '1 to 12'},
{'name': 'Current Day', 'type': 'int', 'value': 'current_day',
'description': 'The day when the notification is triggered.', 'example': '1 to 31'},
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour',
'description': 'The hour when the notification is triggered.', 'example': '0 to 23'},
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute',
'description': 'The minute when the notification is triggered.', 'example': '0 to 59'},
{'name': 'Current Second', 'type': 'int', 'value': 'current_second',
'description': 'The second when the notification is triggered.', 'example': '0 to 59'},
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday',
'description': 'The ISO weekday when the notification is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
{'name': 'Current Week', 'type': 'int', 'value': 'current_week',
'description': 'The ISO week number when the notification is triggered.', 'example': '1 to 52'},
{'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': '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.'},
]
},
},
{
'category': 'Stream Details',
'parameters': [
{'name': 'Streams', 'type': 'int', 'value': 'streams', 'description': 'The total number of concurrent streams.'},
{'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays', 'description': 'The total number of concurrent direct plays.'},
{'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams', 'description': 'The total number of concurrent direct streams.'},
{'name': 'Transcodes', 'type': 'int', 'value': 'transcodes', 'description': 'The total number of concurrent transcodes.'},
{'name': 'Total Bandwidth', 'type': 'int', 'value': 'total_bandwidth', 'description': 'The total Plex Streaming Brain reserved bandwidth (in kbps).', 'help_text': 'not the used bandwidth'},
{'name': 'LAN Bandwidth', 'type': 'int', 'value': 'lan_bandwidth', 'description': 'The total Plex Streaming Brain reserved LAN bandwidth (in kbps).', 'help_text': 'not the used bandwidth'},
{'name': 'WAN Bandwidth', 'type': 'int', 'value': 'wan_bandwidth', 'description': 'The total Plex Streaming Brain reserved WAN bandwidth (in kbps).', 'help_text': 'not the used bandwidth'},
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the user streaming.'},
{'name': 'User Direct Plays', 'type': 'int', 'value': 'user_direct_plays', 'description': 'The number of concurrent direct plays by the user streaming.'},
{'name': 'User Direct Streams', 'type': 'int', 'value': 'user_direct_streams', 'description': 'The number of concurrent direct streams by the user streaming.'},
{'name': 'User Transcodes', 'type': 'int', 'value': 'user_transcodes', 'description': 'The number of concurrent transcodes by the user streaming.'},
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the user streaming.'},
{'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the user streaming.'},
{'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the user streaming.'},
{'name': 'User Thumb', 'type': 'str', 'value': 'user_thumb', 'description': 'The profile picture URL of the user streaming.'},
{'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'},
{'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'},
{'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'},
{'name': 'Player', 'type': 'str', 'value': 'player', 'description': 'The name of the player being used for playback.'},
{'name': 'Initial Stream', 'type': 'int', 'value': 'initial_stream', 'description': 'If the stream is the initial stream of a continuous streaming session.', 'example': '0 or 1'},
{'name': 'IP Address', 'type': 'str', 'value': 'ip_address', 'description': 'The IP address of the device being used for playback.'},
{'name': 'Stream Duration', 'type': 'int', 'value': 'stream_duration', 'description': 'The duration (in minutes) for the stream.'},
{'name': 'Stream Time', 'type': 'str', 'value': 'stream_time', 'description': 'The duration (in time format) of the stream.'},
{'name': 'Remaining Duration', 'type': 'int', 'value': 'remaining_duration', 'description': 'The remaining duration (in minutes) of the stream.'},
{'name': 'Remaining Time', 'type': 'str', 'value': 'remaining_time', 'description': 'The remaining duration (in time format) of the stream.'},
{'name': 'Progress Duration', 'type': 'int', 'value': 'progress_duration', 'description': 'The last reported offset (in minutes) of the stream.'},
{'name': 'Progress Time', 'type': 'str', 'value': 'progress_time', 'description': 'The last reported offset (in time format) of the stream.'},
{'name': 'Progress Percent', 'type': 'int', 'value': 'progress_percent', 'description': 'The last reported progress percent of the stream.'},
{'name': 'Transcode Decision', 'type': 'str', 'value': 'transcode_decision', 'description': 'The transcode decision of the stream.'},
{'name': 'Container Decision', 'type': 'str', 'value': 'container_decision', 'description': 'The container transcode decision of the stream.'},
{'name': 'Video Decision', 'type': 'str', 'value': 'video_decision', 'description': 'The video transcode decision of the stream.'},
{'name': 'Audio Decision', 'type': 'str', 'value': 'audio_decision', 'description': 'The audio transcode decision of the stream.'},
{'name': 'Subtitle Decision', 'type': 'str', 'value': 'subtitle_decision', 'description': 'The subtitle transcode decision of the stream.'},
{'name': 'Quality Profile', 'type': 'str', 'value': 'quality_profile', 'description': 'The Plex quality profile of the stream.', 'example': 'e.g. Original, 4 Mbps 720p, etc.'},
{'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version', 'description': 'If the stream is an optimized version.', 'example': '0 or 1'},
{'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', 'description': 'The optimized version profile of the stream.'},
{'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', 'description': 'If the stream is an synced version.', 'example': '0 or 1'},
{'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.', 'example': '0 or 1'},
{'name': 'Channel Call Sign', 'type': 'str', 'value': 'channel_call_sign', 'description': 'The Live TV channel call sign.'},
{'name': 'Channel Identifier', 'type': 'str', 'value': 'channel_identifier', 'description': 'The Live TV channel number.'},
{'name': 'Channel Thumb', 'type': 'str', 'value': 'channel_thumb', 'description': 'The URL for the Live TV channel logo.'},
{'name': 'Secure', 'type': 'int', 'value': 'secure', 'description': 'If the stream is using a secure connection.', 'example': '0 or 1'},
{'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.', 'example': '0 or 1'},
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'},
{'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', 'description': 'The network location of the stream.', 'example': 'lan or wan'},
{'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The Plex Streaming Brain reserved bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'},
{'name': 'Stream Container', 'type': 'str', 'value': 'stream_container', 'description': 'The media container of the stream.'},
{'name': 'Stream Bitrate', 'type': 'int', 'value': 'stream_bitrate', 'description': 'The bitrate (in kbps) of the stream.'},
{'name': 'Stream Aspect Ratio', 'type': 'float', 'value': 'stream_aspect_ratio', 'description': 'The aspect ratio of the stream.'},
{'name': 'Stream Video Codec', 'type': 'str', 'value': 'stream_video_codec', 'description': 'The video codec of the stream.'},
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', 'description': 'The video codec level of the stream.'},
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'},
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'},
{'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling', 'description': 'The video chroma subsampling of the stream.'},
{'name': 'Stream Video Color Primaries', 'type': 'str', 'value': 'stream_video_color_primaries', 'description': 'The video color primaries of the stream.'},
{'name': 'Stream Video Color Range', 'type': 'str', 'value': 'stream_video_color_range', 'description': 'The video color range of the stream.'},
{'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space', 'description': 'The video color space of the stream.'},
{'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc', 'description': 'The video transfer function of the stream.'},
{'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range', 'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'},
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'},
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'},
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'},
{'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution', 'description': 'The video resolution of the stream.'},
{'name': 'Stream Video Scan Type', 'type': 'str', 'value': 'stream_video_scan_type', 'description': 'The video scan type of the stream.'},
{'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height', 'description': 'The video height of the stream.'},
{'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width', 'description': 'The video width of the stream.'},
{'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language', 'description': 'The video language of the stream.'},
{'name': 'Stream Video Language Code', 'type': 'str', 'value': 'stream_video_language_code', 'description': 'The video language code of the stream.'},
{'name': 'Stream Audio Bitrate', 'type': 'int', 'value': 'stream_audio_bitrate', 'description': 'The audio bitrate of the stream.'},
{'name': 'Stream Audio Bitrate Mode', 'type': 'str', 'value': 'stream_audio_bitrate_mode', 'description': 'The audio bitrate mode of the stream.', 'example': 'cbr or vbr'},
{'name': 'Stream Audio Codec', 'type': 'str', 'value': 'stream_audio_codec', 'description': 'The audio codec of the stream.'},
{'name': 'Stream Audio Channels', 'type': 'float', 'value': 'stream_audio_channels', 'description': 'The audio channels of the stream.'},
{'name': 'Stream Audio Channel Layout', 'type': 'str', 'value': 'stream_audio_channel_layout', 'description': 'The audio channel layout of the stream.'},
{'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the stream.'},
{'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language', 'description': 'The audio language of the stream.'},
{'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code', 'description': 'The audio language code of the stream.'},
{'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec', 'description': 'The subtitle codec of the stream.'},
{'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container', 'description': 'The subtitle container of the stream.'},
{'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format', 'description': 'The subtitle format of the stream.'},
{'name': 'Stream Subtitle Forced', 'type': 'int', 'value': 'stream_subtitle_forced', 'description': 'If the subtitles are forced.', 'example': '0 or 1'},
{'name': 'Stream Subtitle Language', 'type': 'str', 'value': 'stream_subtitle_language', 'description': 'The subtitle language of the stream.'},
{'name': 'Stream Subtitle Language Code', 'type': 'str', 'value': 'stream_subtitle_language_code', 'description': 'The subtitle language code of the stream.'},
{'name': 'Stream Subtitle Location', 'type': 'str', 'value': 'stream_subtitle_location', 'description': 'The subtitle location of the stream.'},
{'name': 'Transcode Container', 'type': 'str', 'value': 'transcode_container', 'description': 'The media container of the transcoded stream.'},
{'name': 'Transcode Video Codec', 'type': 'str', 'value': 'transcode_video_codec', 'description': 'The video codec of the transcoded stream.'},
{'name': 'Transcode Video Width', 'type': 'int', 'value': 'transcode_video_width', 'description': 'The video width of the transcoded stream.'},
{'name': 'Transcode Video Height', 'type': 'int', 'value': 'transcode_video_height', 'description': 'The video height of the transcoded stream.'},
{'name': 'Transcode Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec', 'description': 'The audio codec of the transcoded stream.'},
{'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels', 'description': 'The audio channels of the transcoded stream.'},
{'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.'},
{'name': 'User ID', 'type': 'int', 'value': 'user_id', 'description': 'The unique identifier for the user.'},
{'name': 'Machine ID', 'type': 'str', 'value': 'machine_id', 'description': 'The unique identifier for the player.'},
]
},
{'name': 'Streams', 'type': 'int', 'value': 'streams',
'description': 'The total number of concurrent streams.'},
{'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays',
'description': 'The total number of concurrent direct plays.'},
{'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams',
'description': 'The total number of concurrent direct streams.'},
{'name': 'Transcodes', 'type': 'int', 'value': 'transcodes',
'description': 'The total number of concurrent transcodes.'},
{'name': 'Total Bandwidth', 'type': 'int', 'value': 'total_bandwidth',
'description': 'The total Plex Streaming Brain reserved bandwidth (in kbps).',
'help_text': 'not the used bandwidth'},
{'name': 'LAN Bandwidth', 'type': 'int', 'value': 'lan_bandwidth',
'description': 'The total Plex Streaming Brain reserved LAN bandwidth (in kbps).',
'help_text': 'not the used bandwidth'},
{'name': 'WAN Bandwidth', 'type': 'int', 'value': 'wan_bandwidth',
'description': 'The total Plex Streaming Brain reserved WAN bandwidth (in kbps).',
'help_text': 'not the used bandwidth'},
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams',
'description': 'The number of concurrent streams by the user streaming.'},
{'name': 'User Direct Plays', 'type': 'int', 'value': 'user_direct_plays',
'description': 'The number of concurrent direct plays by the user streaming.'},
{'name': 'User Direct Streams', 'type': 'int', 'value': 'user_direct_streams',
'description': 'The number of concurrent direct streams by the user streaming.'},
{'name': 'User Transcodes', 'type': 'int', 'value': 'user_transcodes',
'description': 'The number of concurrent transcodes by the user streaming.'},
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the user streaming.'},
{'name': 'Username', 'type': 'str', 'value': 'username',
'description': 'The username of the user streaming.'},
{'name': 'User Email', 'type': 'str', 'value': 'user_email',
'description': 'The email address of the user streaming.'},
{'name': 'User Thumb', 'type': 'str', 'value': 'user_thumb',
'description': 'The profile picture URL of the user streaming.'},
{'name': 'Device', 'type': 'str', 'value': 'device',
'description': 'The type of client device being used for playback.'},
{'name': 'Platform', 'type': 'str', 'value': 'platform',
'description': 'The type of client platform being used for playback.'},
{'name': 'Product', 'type': 'str', 'value': 'product',
'description': 'The type of client product being used for playback.'},
{'name': 'Player', 'type': 'str', 'value': 'player',
'description': 'The name of the player being used for playback.'},
{'name': 'Initial Stream', 'type': 'int', 'value': 'initial_stream',
'description': 'If the stream is the initial stream of a continuous streaming session.',
'example': '0 or 1'},
{'name': 'IP Address', 'type': 'str', 'value': 'ip_address',
'description': 'The IP address of the device being used for playback.'},
{'name': 'Stream Duration', 'type': 'int', 'value': 'stream_duration',
'description': 'The duration (in minutes) for the stream.'},
{'name': 'Stream Time', 'type': 'str', 'value': 'stream_time',
'description': 'The duration (in time format) of the stream.'},
{'name': 'Remaining Duration', 'type': 'int', 'value': 'remaining_duration',
'description': 'The remaining duration (in minutes) of the stream.'},
{'name': 'Remaining Time', 'type': 'str', 'value': 'remaining_time',
'description': 'The remaining duration (in time format) of the stream.'},
{'name': 'Progress Duration', 'type': 'int', 'value': 'progress_duration',
'description': 'The last reported offset (in minutes) of the stream.'},
{'name': 'Progress Time', 'type': 'str', 'value': 'progress_time',
'description': 'The last reported offset (in time format) of the stream.'},
{'name': 'Progress Percent', 'type': 'int', 'value': 'progress_percent',
'description': 'The last reported progress percent of the stream.'},
{'name': 'Transcode Decision', 'type': 'str', 'value': 'transcode_decision',
'description': 'The transcode decision of the stream.'},
{'name': 'Container Decision', 'type': 'str', 'value': 'container_decision',
'description': 'The container transcode decision of the stream.'},
{'name': 'Video Decision', 'type': 'str', 'value': 'video_decision',
'description': 'The video transcode decision of the stream.'},
{'name': 'Audio Decision', 'type': 'str', 'value': 'audio_decision',
'description': 'The audio transcode decision of the stream.'},
{'name': 'Subtitle Decision', 'type': 'str', 'value': 'subtitle_decision',
'description': 'The subtitle transcode decision of the stream.'},
{'name': 'Quality Profile', 'type': 'str', 'value': 'quality_profile',
'description': 'The Plex quality profile of the stream.', 'example': 'e.g. Original, 4 Mbps 720p, etc.'},
{'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version',
'description': 'If the stream is an optimized version.', 'example': '0 or 1'},
{'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile',
'description': 'The optimized version profile of the stream.'},
{'name': 'Synced Version', 'type': 'int', 'value': 'synced_version',
'description': 'If the stream is an synced version.', 'example': '0 or 1'},
{'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.',
'example': '0 or 1'},
{'name': 'Channel Call Sign', 'type': 'str', 'value': 'channel_call_sign',
'description': 'The Live TV channel call sign.'},
{'name': 'Channel Identifier', 'type': 'str', 'value': 'channel_identifier',
'description': 'The Live TV channel number.'},
{'name': 'Channel Thumb', 'type': 'str', 'value': 'channel_thumb',
'description': 'The URL for the Live TV channel logo.'},
{'name': 'Secure', 'type': 'int', 'value': 'secure',
'description': 'If the stream is using a secure connection.', 'example': '0 or 1'},
{'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.',
'example': '0 or 1'},
{'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.',
'example': '0 or 1'},
{'name': 'Stream Location', 'type': 'str', 'value': 'stream_location',
'description': 'The network location of the stream.', 'example': 'lan or wan'},
{'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth',
'description': 'The Plex Streaming Brain reserved bandwidth (in kbps) of the stream.',
'help_text': 'not the used bandwidth'},
{'name': 'Stream Container', 'type': 'str', 'value': 'stream_container',
'description': 'The media container of the stream.'},
{'name': 'Stream Bitrate', 'type': 'int', 'value': 'stream_bitrate',
'description': 'The bitrate (in kbps) of the stream.'},
{'name': 'Stream Aspect Ratio', 'type': 'float', 'value': 'stream_aspect_ratio',
'description': 'The aspect ratio of the stream.'},
{'name': 'Stream Video Codec', 'type': 'str', 'value': 'stream_video_codec',
'description': 'The video codec of the stream.'},
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level',
'description': 'The video codec level of the stream.'},
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate',
'description': 'The video bitrate (in kbps) of the stream.'},
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth',
'description': 'The video bit depth of the stream.'},
{'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling',
'description': 'The video chroma subsampling of the stream.'},
{'name': 'Stream Video Color Primaries', 'type': 'str', 'value': 'stream_video_color_primaries',
'description': 'The video color primaries of the stream.'},
{'name': 'Stream Video Color Range', 'type': 'str', 'value': 'stream_video_color_range',
'description': 'The video color range of the stream.'},
{'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space',
'description': 'The video color space of the stream.'},
{'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc',
'description': 'The video transfer function of the stream.'},
{'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range',
'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'},
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate',
'description': 'The video framerate of the stream.'},
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution',
'description': 'The video resolution of the stream with scan type.'},
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames',
'description': 'The video reference frames of the stream.'},
{'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution',
'description': 'The video resolution of the stream.'},
{'name': 'Stream Video Scan Type', 'type': 'str', 'value': 'stream_video_scan_type',
'description': 'The video scan type of the stream.'},
{'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height',
'description': 'The video height of the stream.'},
{'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width',
'description': 'The video width of the stream.'},
{'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language',
'description': 'The video language of the stream.'},
{'name': 'Stream Video Language Code', 'type': 'str', 'value': 'stream_video_language_code',
'description': 'The video language code of the stream.'},
{'name': 'Stream Audio Bitrate', 'type': 'int', 'value': 'stream_audio_bitrate',
'description': 'The audio bitrate of the stream.'},
{'name': 'Stream Audio Bitrate Mode', 'type': 'str', 'value': 'stream_audio_bitrate_mode',
'description': 'The audio bitrate mode of the stream.', 'example': 'cbr or vbr'},
{'name': 'Stream Audio Codec', 'type': 'str', 'value': 'stream_audio_codec',
'description': 'The audio codec of the stream.'},
{'name': 'Stream Audio Channels', 'type': 'float', 'value': 'stream_audio_channels',
'description': 'The audio channels of the stream.'},
{'name': 'Stream Audio Channel Layout', 'type': 'str', 'value': 'stream_audio_channel_layout',
'description': 'The audio channel layout of the stream.'},
{'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate',
'description': 'The audio sample rate (in Hz) of the stream.'},
{'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language',
'description': 'The audio language of the stream.'},
{'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code',
'description': 'The audio language code of the stream.'},
{'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec',
'description': 'The subtitle codec of the stream.'},
{'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container',
'description': 'The subtitle container of the stream.'},
{'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format',
'description': 'The subtitle format of the stream.'},
{'name': 'Stream Subtitle Forced', 'type': 'int', 'value': 'stream_subtitle_forced',
'description': 'If the subtitles are forced.', 'example': '0 or 1'},
{'name': 'Stream Subtitle Language', 'type': 'str', 'value': 'stream_subtitle_language',
'description': 'The subtitle language of the stream.'},
{'name': 'Stream Subtitle Language Code', 'type': 'str', 'value': 'stream_subtitle_language_code',
'description': 'The subtitle language code of the stream.'},
{'name': 'Stream Subtitle Location', 'type': 'str', 'value': 'stream_subtitle_location',
'description': 'The subtitle location of the stream.'},
{'name': 'Transcode Container', 'type': 'str', 'value': 'transcode_container',
'description': 'The media container of the transcoded stream.'},
{'name': 'Transcode Video Codec', 'type': 'str', 'value': 'transcode_video_codec',
'description': 'The video codec of the transcoded stream.'},
{'name': 'Transcode Video Width', 'type': 'int', 'value': 'transcode_video_width',
'description': 'The video width of the transcoded stream.'},
{'name': 'Transcode Video Height', 'type': 'int', 'value': 'transcode_video_height',
'description': 'The video height of the transcoded stream.'},
{'name': 'Transcode Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec',
'description': 'The audio codec of the transcoded stream.'},
{'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels',
'description': 'The audio channels of the transcoded stream.'},
{'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.'},
{'name': 'User ID', 'type': 'int', 'value': 'user_id',
'description': 'The unique identifier for the user.'},
{'name': 'Machine ID', 'type': 'str', 'value': 'machine_id',
'description': 'The unique identifier for the player.'},
]
},
{
'category': 'Source Metadata Details',
'parameters': [
{'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', 'example': 'movie, show, season, episode, artist, album, track, clip'},
{'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'},
{'name': 'Library Name', 'type': 'str', 'value': 'library_name', 'description': 'The library name of the item.'},
{'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'},
{'name': 'Episode Name', 'type': 'str', 'value': 'episode_name', 'description': 'The title of the episode.'},
{'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'},
{'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'},
{'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'},
{'name': 'Track Artist', 'type': 'str', 'value': 'track_artist', 'description': 'The name of the artist of the track.'},
{'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.', 'example': 'e.g. 1, or 1-3'},
{'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00', 'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'},
{'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', 'example': 'e.g. 6, or 6-10'},
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'},
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
{'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'},
{'name': 'Episode Count', 'type': 'int', 'value': 'episode_count', 'description': 'The number of episodes.'},
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
{'name': 'Release Date', 'type': 'str', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'},
{'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'},
{'name': 'Added Date', 'type': 'str', 'value': 'added_date', 'description': 'The date (in date format) the item was added to Plex.'},
{'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'},
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
{'name': 'Content Rating', 'type': 'str', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
{'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
{'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
{'name': 'Collections', 'type': 'str', 'value': 'collections', 'description': 'A list of collections for the item.'},
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
{'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating', 'description': 'The critic rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
{'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating', 'description': 'The audience rating for the item.', 'help_text': 'Rating out of 10 for IMDB, percentage (%) for Rotten Tomatoes and TMDB.'},
{'name': 'User Rating', 'type': 'float', 'value': 'user_rating', 'description': 'The user (star) rating (out of 10) for the item.'},
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
{'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.', 'example': 'e.g. 5d7769a9594b2b001e6a6b7e'},
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'},
{'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.', 'example': 'e.g. tt2488496'},
{'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'},
{'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the TV show.', 'example': 'e.g. 121361'},
{'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the TV show.'},
{'name': 'TMDB ID', 'type': 'int', 'value': 'themoviedb_id', 'description': 'The TMDb ID for the movie or TV show.', 'example': 'e.g. 15260'},
{'name': 'TMDB URL', 'type': 'str', 'value': 'themoviedb_url', 'description': 'The TMDb URL for the movie or TV show.'},
{'name': 'TVmaze ID', 'type': 'int', 'value': 'tvmaze_id', 'description': 'The TVmaze ID for the TV show.', 'example': 'e.g. 290'},
{'name': 'TVmaze URL', 'type': 'str', 'value': 'tvmaze_url', 'description': 'The TVmaze URL for the TV show.'},
{'name': 'MusicBrainz ID', 'type': 'str', 'value': 'musicbrainz_id', 'description': 'The MusicBrainz ID for the artist, album, or track.', 'example': 'e.g. b670dfcf-9824-4309-a57e-03595aaba286'},
{'name': 'MusicBrainz URL', 'type': 'str', 'value': 'musicbrainz_url', 'description': 'The MusicBrainz URL for the artist, album, or track.'},
{'name': 'Last.fm URL', 'type': 'str', 'value': 'lastfm_url', 'description': 'The Last.fm URL for the album.', 'help_text': 'Music library agent must be Last.fm'},
{'name': 'Trakt.tv URL', 'type': 'str', 'value': 'trakt_url', 'description': 'The trakt.tv URL for the movie or TV show.'},
{'name': 'Container', 'type': 'str', 'value': 'container', 'description': 'The media container of the original media.'},
{'name': 'Bitrate', 'type': 'int', 'value': 'bitrate', 'description': 'The bitrate of the original media.'},
{'name': 'Aspect Ratio', 'type': 'float', 'value': 'aspect_ratio', 'description': 'The aspect ratio of the original media.'},
{'name': 'Video Codec', 'type': 'str', 'value': 'video_codec', 'description': 'The video codec of the original media.'},
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', 'description': 'The video codec level of the original media.'},
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'},
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'},
{'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling', 'description': 'The video chroma subsampling of the original media.'},
{'name': 'Video Color Primaries', 'type': 'str', 'value': 'video_color_primaries', 'description': 'The video color primaries of the original media.'},
{'name': 'Video Color Range', 'type': 'str', 'value': 'video_color_range', 'description': 'The video color range of the original media.'},
{'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space', 'description': 'The video color space of the original media.'},
{'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc', 'description': 'The video transfer function of the original media.'},
{'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range', 'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'},
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'},
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'},
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'},
{'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution', 'description': 'The video resolution of the original media.'},
{'name': 'Video Scan Type', 'type': 'str', 'value': 'video_scan_type', 'description': 'The video scan type of the original media.'},
{'name': 'Video Height', 'type': 'int', 'value': 'video_height', 'description': 'The video height of the original media.'},
{'name': 'Video Width', 'type': 'int', 'value': 'video_width', 'description': 'The video width of the original media.'},
{'name': 'Video Language', 'type': 'str', 'value': 'video_language', 'description': 'The video language of the original media.'},
{'name': 'Video Language Code', 'type': 'str', 'value': 'video_language_code', 'description': 'The video language code of the original media.'},
{'name': 'Audio Bitrate', 'type': 'int', 'value': 'audio_bitrate', 'description': 'The audio bitrate of the original media.'},
{'name': 'Audio Bitrate Mode', 'type': 'str', 'value': 'audio_bitrate_mode', 'description': 'The audio bitrate mode of the original media.', 'example': 'cbr or vbr'},
{'name': 'Audio Codec', 'type': 'str', 'value': 'audio_codec', 'description': 'The audio codec of the original media.'},
{'name': 'Audio Channels', 'type': 'float', 'value': 'audio_channels', 'description': 'The audio channels of the original media.'},
{'name': 'Audio Channel Layout', 'type': 'str', 'value': 'audio_channel_layout', 'description': 'The audio channel layout of the original media.'},
{'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the original media.'},
{'name': 'Audio Language', 'type': 'str', 'value': 'audio_language', 'description': 'The audio language of the original media.'},
{'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code', 'description': 'The audio language code of the original media.'},
{'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec', 'description': 'The subtitle codec of the original media.'},
{'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container', 'description': 'The subtitle container of the original media.'},
{'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format', 'description': 'The subtitle format of the original media.'},
{'name': 'Subtitle Forced', 'type': 'int', 'value': 'subtitle_forced', 'description': 'If the subtitles are forced.', 'example': '0 or 1'},
{'name': 'Subtitle Location', 'type': 'str', 'value': 'subtitle_location', 'description': 'The subtitle location of the original media.'},
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'},
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
{'name': 'Parent Rating Key', 'type': 'int', 'value': 'parent_rating_key', 'description': 'The unique identifier for the season or album.'},
{'name': 'Grandparent Rating Key', 'type': 'int', 'value': 'grandparent_rating_key', 'description': 'The unique identifier for the TV show or artist.'},
{'name': 'Art', 'type': 'str', 'value': 'art', 'description': 'The Plex background art for the media.'},
{'name': 'Thumb', 'type': 'str', 'value': 'thumb', 'description': 'The Plex thumbnail for the movie or episode.'},
{'name': 'Parent Thumb', 'type': 'str', 'value': 'parent_thumb', 'description': 'The Plex thumbnail for the season or album.'},
{'name': 'Grandparent Thumb', 'type': 'str', 'value': 'grandparent_thumb', 'description': 'The Plex thumbnail for the TV show or artist.'},
{'name': 'Poster Thumb', 'type': 'str', 'value': 'poster_thumb', 'description': 'The Plex thumbnail for the poster image.'},
{'name': 'Poster Title', 'type': 'str', 'value': 'poster_title', 'description': 'The title for the poster image.'},
{'name': 'Indexes', 'type': 'int', 'value': 'indexes', 'description': 'If the media has video preview thumbnails.', 'example': '0 or 1'},
]
},
{'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.',
'example': 'movie, show, season, episode, artist, album, track, clip'},
{'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'},
{'name': 'Library Name', 'type': 'str', 'value': 'library_name',
'description': 'The library name of the item.'},
{'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'},
{'name': 'Episode Name', 'type': 'str', 'value': 'episode_name',
'description': 'The title of the episode.'},
{'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'},
{'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'},
{'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'},
{'name': 'Track Artist', 'type': 'str', 'value': 'track_artist',
'description': 'The name of the artist of the track.'},
{'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.',
'example': 'e.g. 1, or 1-3'},
{'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00',
'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'},
{'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.',
'example': 'e.g. 6, or 6-10'},
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00',
'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.',
'example': 'e.g. 4, or 4-10'},
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00',
'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
{'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'},
{'name': 'Episode Count', 'type': 'int', 'value': 'episode_count',
'description': 'The number of episodes.'},
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
{'name': 'Release Date', 'type': 'str', 'value': 'release_date',
'description': 'The release date (in date format) for the item.'},
{'name': 'Air Date', 'type': 'str', 'value': 'air_date',
'description': 'The air date (in date format) for the item.'},
{'name': 'Added Date', 'type': 'str', 'value': 'added_date',
'description': 'The date (in date format) the item was added to Plex.'},
{'name': 'Updated Date', 'type': 'str', 'value': 'updated_date',
'description': 'The date (in date format) the item was updated on Plex.'},
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date',
'description': 'The date (in date format) the item was last viewed on Plex.'},
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
{'name': 'Content Rating', 'type': 'str', 'value': 'content_rating',
'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
{'name': 'Directors', 'type': 'str', 'value': 'directors',
'description': 'A list of directors for the item.'},
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
{'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'},
{'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'},
{'name': 'Collections', 'type': 'str', 'value': 'collections',
'description': 'A list of collections for the item.'},
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
{'name': 'Rating', 'type': 'float', 'value': 'rating',
'description': 'The rating (out of 10) for the item.'},
{'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating',
'description': 'The critic rating (%) for the item.',
'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
{'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating',
'description': 'The audience rating for the item.',
'help_text': 'Rating out of 10 for IMDB, percentage (%) for Rotten Tomatoes and TMDB.'},
{'name': 'User Rating', 'type': 'float', 'value': 'user_rating',
'description': 'The user (star) rating (out of 10) for the item.'},
{'name': 'Duration', 'type': 'int', 'value': 'duration',
'description': 'The duration (in minutes) for the item.'},
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url',
'description': 'A URL for the movie, TV show, or album poster.'},
{'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.',
'example': 'e.g. 5d7769a9594b2b001e6a6b7e'},
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url',
'description': 'The Plex URL to your server for the item.'},
{'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.',
'example': 'e.g. tt2488496'},
{'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'},
{'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the TV show.',
'example': 'e.g. 121361'},
{'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the TV show.'},
{'name': 'TMDB ID', 'type': 'int', 'value': 'themoviedb_id',
'description': 'The TMDb ID for the movie or TV show.', 'example': 'e.g. 15260'},
{'name': 'TMDB URL', 'type': 'str', 'value': 'themoviedb_url',
'description': 'The TMDb URL for the movie or TV show.'},
{'name': 'TVmaze ID', 'type': 'int', 'value': 'tvmaze_id', 'description': 'The TVmaze ID for the TV show.',
'example': 'e.g. 290'},
{'name': 'TVmaze URL', 'type': 'str', 'value': 'tvmaze_url',
'description': 'The TVmaze URL for the TV show.'},
{'name': 'MusicBrainz ID', 'type': 'str', 'value': 'musicbrainz_id',
'description': 'The MusicBrainz ID for the artist, album, or track.',
'example': 'e.g. b670dfcf-9824-4309-a57e-03595aaba286'},
{'name': 'MusicBrainz URL', 'type': 'str', 'value': 'musicbrainz_url',
'description': 'The MusicBrainz URL for the artist, album, or track.'},
{'name': 'Last.fm URL', 'type': 'str', 'value': 'lastfm_url',
'description': 'The Last.fm URL for the album.', 'help_text': 'Music library agent must be Last.fm'},
{'name': 'Trakt.tv URL', 'type': 'str', 'value': 'trakt_url',
'description': 'The trakt.tv URL for the movie or TV show.'},
{'name': 'Container', 'type': 'str', 'value': 'container',
'description': 'The media container of the original media.'},
{'name': 'Bitrate', 'type': 'int', 'value': 'bitrate', 'description': 'The bitrate of the original media.'},
{'name': 'Aspect Ratio', 'type': 'float', 'value': 'aspect_ratio',
'description': 'The aspect ratio of the original media.'},
{'name': 'Video Codec', 'type': 'str', 'value': 'video_codec',
'description': 'The video codec of the original media.'},
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level',
'description': 'The video codec level of the original media.'},
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate',
'description': 'The video bitrate of the original media.'},
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth',
'description': 'The video bit depth of the original media.'},
{'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling',
'description': 'The video chroma subsampling of the original media.'},
{'name': 'Video Color Primaries', 'type': 'str', 'value': 'video_color_primaries',
'description': 'The video color primaries of the original media.'},
{'name': 'Video Color Range', 'type': 'str', 'value': 'video_color_range',
'description': 'The video color range of the original media.'},
{'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space',
'description': 'The video color space of the original media.'},
{'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc',
'description': 'The video transfer function of the original media.'},
{'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range',
'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'},
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate',
'description': 'The video framerate of the original media.'},
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution',
'description': 'The video resolution of the original media with scan type.'},
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames',
'description': 'The video reference frames of the original media.'},
{'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution',
'description': 'The video resolution of the original media.'},
{'name': 'Video Scan Type', 'type': 'str', 'value': 'video_scan_type',
'description': 'The video scan type of the original media.'},
{'name': 'Video Height', 'type': 'int', 'value': 'video_height',
'description': 'The video height of the original media.'},
{'name': 'Video Width', 'type': 'int', 'value': 'video_width',
'description': 'The video width of the original media.'},
{'name': 'Video Language', 'type': 'str', 'value': 'video_language',
'description': 'The video language of the original media.'},
{'name': 'Video Language Code', 'type': 'str', 'value': 'video_language_code',
'description': 'The video language code of the original media.'},
{'name': 'Audio Bitrate', 'type': 'int', 'value': 'audio_bitrate',
'description': 'The audio bitrate of the original media.'},
{'name': 'Audio Bitrate Mode', 'type': 'str', 'value': 'audio_bitrate_mode',
'description': 'The audio bitrate mode of the original media.', 'example': 'cbr or vbr'},
{'name': 'Audio Codec', 'type': 'str', 'value': 'audio_codec',
'description': 'The audio codec of the original media.'},
{'name': 'Audio Channels', 'type': 'float', 'value': 'audio_channels',
'description': 'The audio channels of the original media.'},
{'name': 'Audio Channel Layout', 'type': 'str', 'value': 'audio_channel_layout',
'description': 'The audio channel layout of the original media.'},
{'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate',
'description': 'The audio sample rate (in Hz) of the original media.'},
{'name': 'Audio Language', 'type': 'str', 'value': 'audio_language',
'description': 'The audio language of the original media.'},
{'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code',
'description': 'The audio language code of the original media.'},
{'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec',
'description': 'The subtitle codec of the original media.'},
{'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container',
'description': 'The subtitle container of the original media.'},
{'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format',
'description': 'The subtitle format of the original media.'},
{'name': 'Subtitle Forced', 'type': 'int', 'value': 'subtitle_forced',
'description': 'If the subtitles are forced.', 'example': '0 or 1'},
{'name': 'Subtitle Location', 'type': 'str', 'value': 'subtitle_location',
'description': 'The subtitle location of the original media.'},
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language',
'description': 'The subtitle language of the original media.'},
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code',
'description': 'The subtitle language code of the original media.'},
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
{'name': 'Section ID', 'type': 'int', 'value': 'section_id',
'description': 'The unique identifier for the library.'},
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key',
'description': 'The unique identifier for the movie, episode, or track.'},
{'name': 'Parent Rating Key', 'type': 'int', 'value': 'parent_rating_key',
'description': 'The unique identifier for the season or album.'},
{'name': 'Grandparent Rating Key', 'type': 'int', 'value': 'grandparent_rating_key',
'description': 'The unique identifier for the TV show or artist.'},
{'name': 'Art', 'type': 'str', 'value': 'art', 'description': 'The Plex background art for the media.'},
{'name': 'Thumb', 'type': 'str', 'value': 'thumb',
'description': 'The Plex thumbnail for the movie or episode.'},
{'name': 'Parent Thumb', 'type': 'str', 'value': 'parent_thumb',
'description': 'The Plex thumbnail for the season or album.'},
{'name': 'Grandparent Thumb', 'type': 'str', 'value': 'grandparent_thumb',
'description': 'The Plex thumbnail for the TV show or artist.'},
{'name': 'Poster Thumb', 'type': 'str', 'value': 'poster_thumb',
'description': 'The Plex thumbnail for the poster image.'},
{'name': 'Poster Title', 'type': 'str', 'value': 'poster_title',
'description': 'The title for the poster image.'},
{'name': 'Indexes', 'type': 'int', 'value': 'indexes',
'description': 'If the media has video preview thumbnails.', 'example': '0 or 1'},
]
},
{
'category': 'Plex Remote Access',
'parameters': [
{'name': 'Remote Access Mapping State', 'type': 'str', 'value': 'remote_access_mapping_state', 'description': 'The mapping state of the Plex remote access port.'},
{'name': 'Remote Access Mapping Error', 'type': 'str', 'value': 'remote_access_mapping_error', 'description': 'The mapping error of the Plex remote access port.'},
{'name': 'Remote Access Public IP Address', 'type': 'str', 'value': 'remote_access_public_address', 'description': 'The Plex remote access public IP address.'},
{'name': 'Remote Access Public Port', 'type': 'str', 'value': 'remote_access_public_port', 'description': 'The Plex remote access public port.'},
{'name': 'Remote Access Private IP Address', 'type': 'str', 'value': 'remote_access_private_address', 'description': 'The Plex remote access private IP address.'},
{'name': 'Remote Access Private Port', 'type': 'str', 'value': 'remote_access_private_port', 'description': 'The Plex remote access private port.'},
{'name': 'Remote Access Failure Reason', 'type': 'str', 'value': 'remote_access_reason', 'description': 'The failure reason for Plex remote access going down.'},
{'name': 'Remote Access Mapping State', 'type': 'str', 'value': 'remote_access_mapping_state',
'description': 'The mapping state of the Plex remote access port.'},
{'name': 'Remote Access Mapping Error', 'type': 'str', 'value': 'remote_access_mapping_error',
'description': 'The mapping error of the Plex remote access port.'},
{'name': 'Remote Access Public IP Address', 'type': 'str', 'value': 'remote_access_public_address',
'description': 'The Plex remote access public IP address.'},
{'name': 'Remote Access Public Port', 'type': 'str', 'value': 'remote_access_public_port',
'description': 'The Plex remote access public port.'},
{'name': 'Remote Access Private IP Address', 'type': 'str', 'value': 'remote_access_private_address',
'description': 'The Plex remote access private IP address.'},
{'name': 'Remote Access Private Port', 'type': 'str', 'value': 'remote_access_private_port',
'description': 'The Plex remote access private port.'},
{'name': 'Remote Access Failure Reason', 'type': 'str', 'value': 'remote_access_reason',
'description': 'The failure reason for Plex remote access going down.'},
]
},
},
{
'category': 'Plex Update Available',
'parameters': [
{'name': 'Update Version', 'type': 'str', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'},
{'name': 'Update Url', 'type': 'str', 'value': 'update_url', 'description': 'The download URL for the available update.'},
{'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', 'description': 'The release date of the available update.'},
{'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'},
{'name': 'Update Platform', 'type': 'str', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'},
{'name': 'Update Distro', 'type': 'str', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'},
{'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build', 'description': 'The distro build of your Plex Server.'},
{'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements', 'description': 'The requirements for the available update.'},
{'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info', 'description': 'Any extra info for the available update.'},
{'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added', 'description': 'The added changelog for the available update.'},
{'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed', 'description': 'The fixed changelog for the available update.'},
]
},
{'name': 'Update Version', 'type': 'str', 'value': 'update_version',
'description': 'The available update version for your Plex Server.'},
{'name': 'Update Url', 'type': 'str', 'value': 'update_url',
'description': 'The download URL for the available update.'},
{'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date',
'description': 'The release date of the available update.'},
{'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.',
'example': 'Public or Plex Pass'},
{'name': 'Update Platform', 'type': 'str', 'value': 'update_platform',
'description': 'The platform of your Plex Server.'},
{'name': 'Update Distro', 'type': 'str', 'value': 'update_distro',
'description': 'The distro of your Plex Server.'},
{'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build',
'description': 'The distro build of your Plex Server.'},
{'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements',
'description': 'The requirements for the available update.'},
{'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info',
'description': 'Any extra info for the available update.'},
{'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added',
'description': 'The added changelog for the available update.'},
{'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed',
'description': 'The fixed changelog for the available update.'},
]
},
{
'category': 'Tautulli Update Available',
'parameters': [
{'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'},
{'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url', 'description': 'The release page URL on GitHub.'},
{'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'},
{'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'},
{'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'},
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'},
{'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'},
{'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version',
'description': 'The available update version for Tautulli.'},
{'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url',
'description': 'The release page URL on GitHub.'},
{'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar',
'description': 'The tar download URL for the available update.'},
{'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip',
'description': 'The zip download URL for the available update.'},
{'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit',
'description': 'The commit hash for the available update.'},
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind',
'description': 'The number of commits behind for the available update.'},
{'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog',
'description': 'The changelog for the available update.'},
]
},
},
]
NEWSLETTER_PARAMETERS = [
{
'category': 'Global',
'parameters': [
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
{'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'},
{'name': 'Server Name', 'type': 'str', 'value': 'server_name',
'description': 'The name of your Plex Server.'},
{'name': 'Start Date', 'type': 'str', 'value': 'start_date',
'description': 'The start date of the newsletter.'},
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'},
{'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year of the start date of the newsletter.'},
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'},
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'},
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'},
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'},
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'},
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'},
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'},
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'},
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'},
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'},
{'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url', 'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'},
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'},
{'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', 'description': 'The unique ID number for the newsletter agent.'},
{'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', 'description': 'The unique ID name for the newsletter agent.'},
{'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password', 'description': 'The password required to view the newsletter if enabled.'},
{'name': 'Current Year', 'type': 'int', 'value': 'current_year',
'description': 'The year of the start date of the newsletter.'},
{'name': 'Current Month', 'type': 'int', 'value': 'current_month',
'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'},
{'name': 'Current Day', 'type': 'int', 'value': 'current_day',
'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'},
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour',
'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'},
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute',
'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'},
{'name': 'Current Second', 'type': 'int', 'value': 'current_second',
'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'},
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday',
'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'},
{'name': 'Current Week', 'type': 'int', 'value': 'current_week',
'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'},
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame',
'description': 'The time frame included in the newsletter.'},
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units',
'description': 'The time frame units included in the newsletter.'},
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url',
'description': 'The self-hosted URL to the newsletter.'},
{'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url',
'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'},
{'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid',
'description': 'The unique identifier for the newsletter.'},
{'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id',
'description': 'The unique ID number for the newsletter agent.'},
{'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name',
'description': 'The unique ID name for the newsletter agent.'},
{'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password',
'description': 'The password required to view the newsletter if enabled.'},
]
},
},
{
'category': 'Recently Added',
'parameters': [
{'name': 'Included Libraries', 'type': 'str', 'value': 'newsletter_libraries', 'description': 'The list of libraries included in the newsletter.'},
{'name': 'Included Libraries', 'type': 'str', 'value': 'newsletter_libraries',
'description': 'The list of libraries included in the newsletter.'},
]
}
]

View File

@@ -13,25 +13,17 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import object
from future.builtins import str
import os
import re
import shutil
import time
import threading
import time
from configobj import ConfigObj, ParseError
import jellypy
if jellypy.PYTHON2:
import helpers
import logger
else:
from jellypy import helpers
from jellypy import logger
from jellypy import helpers
from jellypy import logger
def bool_int(value):
@@ -114,7 +106,8 @@ _CONFIG_DEFINITIONS = {
'HOME_SECTIONS': (list, 'General', ['current_activity', 'watch_stats', 'library_stats', 'recently_added']),
'HOME_LIBRARY_CARDS': (list, 'General', ['first_run']),
'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music',
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
'popular_music', 'last_watched', 'top_users', 'top_platforms',
'most_concurrent']),
'HOME_REFRESH_INTERVAL': (int, 'General', 10),
'HTTPS_CREATE_CERT': (int, 'General', 1),
'HTTPS_CERT': (str, 'General', ''),
@@ -337,7 +330,7 @@ class Config(object):
for key, subkeys in self._config.items():
for subkey, value in subkeys.items():
if isinstance(value, str) and len(value.strip()) > 5 and \
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
blacklist.add(value.strip())
logger._BLACKLIST_WORDS.update(blacklist)

View File

@@ -13,24 +13,16 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import object
import os
import sqlite3
import shutil
import sqlite3
import threading
import time
import jellypy
if jellypy.PYTHON2:
import helpers
import logger
else:
from jellypy import helpers
from jellypy import logger
from jellypy import helpers
from jellypy import logger
FILENAME = "tautulli.db"
db_lock = threading.Lock()
@@ -455,8 +447,9 @@ class MonitorDatabase(object):
if self.connection.total_changes == changes_before:
trans_type = 'insert'
insert_query = (
"INSERT INTO " + table_name + " (" + ", ".join(list(value_dict.keys()) + list(key_dict.keys())) + ")" +
" VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")"
"INSERT INTO " + table_name + " (" + ", ".join(
list(value_dict.keys()) + list(key_dict.keys())) + ")" +
" VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")"
)
try:
self.action(insert_query, list(value_dict.values()) + list(key_dict.values()))
@@ -470,4 +463,4 @@ class MonitorDatabase(object):
# Get the last insert row id
result = self.select_single(query='SELECT last_insert_rowid() AS last_id')
if result:
return result.get('last_id', None)
return result.get('last_id', None)

View File

@@ -15,32 +15,18 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
from future.builtins import object
import json
from itertools import groupby
import jellypy
if jellypy.PYTHON2:
import common
import database
import datatables
import helpers
import logger
import pmsconnect
import session
else:
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import logger
from jellypy import pmsconnect
from jellypy import session
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import logger
from jellypy import pmsconnect
from jellypy import session
class DataFactory(object):
@@ -127,7 +113,7 @@ class DataFactory(object):
'GROUP_CONCAT(session_history.id) AS group_ids',
'NULL AS state',
'NULL AS session_key'
]
]
if include_activity:
table_name_union = 'sessions'
@@ -180,7 +166,7 @@ class DataFactory(object):
'NULL AS group_ids',
'state',
'session_key'
]
]
else:
table_name_union = None
@@ -343,7 +329,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_movies: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_movies: %s." % e)
return None
for item in result:
@@ -395,7 +382,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_movies: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_movies: %s." % e)
return None
for item in result:
@@ -444,7 +432,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_tv: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_tv: %s." % e)
return None
for item in result:
@@ -496,7 +485,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_tv: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_tv: %s." % e)
return None
for item in result:
@@ -546,7 +536,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_music: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_music: %s." % e)
return None
for item in result:
@@ -599,7 +590,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_music: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_music: %s." % e)
return None
for item in result:
@@ -649,7 +641,8 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_users: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_users: %s." % e)
return None
for item in result:
@@ -674,7 +667,7 @@ class DataFactory(object):
'title': '',
'platform': '',
'row_id': ''
}
}
top_users.append(row)
home_stats.append({'stat_id': stat,
@@ -701,13 +694,15 @@ class DataFactory(object):
'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_platforms: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_platforms: %s." % e)
return None
for item in result:
# Rename Mystery platform names
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default')
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()),
'default')
row = {'total_plays': item['total_plays'],
'total_duration': item['total_duration'],
@@ -759,7 +754,8 @@ class DataFactory(object):
stats_count, stats_start)
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: last_watched: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: last_watched: %s." % e)
return None
for item in result:
@@ -848,26 +844,27 @@ class DataFactory(object):
title = 'Concurrent Transcodes'
query = base_query \
+ 'AND session_history_media_info.transcode_decision = "transcode" '
+ 'AND session_history_media_info.transcode_decision = "transcode" '
result = monitor_db.select(query)
if result:
most_concurrent.append(calc_most_concurrent(title, result))
title = 'Concurrent Direct Streams'
query = base_query \
+ 'AND session_history_media_info.transcode_decision = "copy" '
+ 'AND session_history_media_info.transcode_decision = "copy" '
result = monitor_db.select(query)
if result:
most_concurrent.append(calc_most_concurrent(title, result))
title = 'Concurrent Direct Plays'
query = base_query \
+ 'AND session_history_media_info.transcode_decision = "direct play" '
+ 'AND session_history_media_info.transcode_decision = "direct play" '
result = monitor_db.select(query)
if result:
most_concurrent.append(calc_most_concurrent(title, result))
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: most_concurrent: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_home_stats: most_concurrent: %s." % e)
return None
home_stats.append({'stat_id': stat,
@@ -1577,14 +1574,14 @@ class DataFactory(object):
key = item['parent_media_index'] if match_type == 'index' else item['parent_title']
parents.update({key:
{'rating_key': item['parent_rating_key'],
'children': children}
{'rating_key': item['parent_rating_key'],
'children': children}
})
key = 0 if match_type == 'index' else item['grandparent_title']
grandparents.update({key:
{'rating_key': item['grandparent_rating_key'],
'children': parents}
{'rating_key': item['grandparent_rating_key'],
'children': parents}
})
key_list = grandparents
@@ -1619,16 +1616,20 @@ class DataFactory(object):
if metadata:
if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
# check grandparent_rating_key (2 tables)
monitor_db.action('UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
[new_key, old_key])
monitor_db.action('UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
[new_key, old_key])
monitor_db.action(
'UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
[new_key, old_key])
monitor_db.action(
'UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
[new_key, old_key])
elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album':
# check parent_rating_key (2 tables)
monitor_db.action('UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?',
[new_key, old_key])
monitor_db.action('UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?',
[new_key, old_key])
monitor_db.action(
'UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?',
[new_key, old_key])
monitor_db.action(
'UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?',
[new_key, old_key])
else:
# check rating_key (2 tables)
monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?',
@@ -1661,7 +1662,7 @@ class DataFactory(object):
genres = ";".join(metadata['genres'])
labels = ";".join(metadata['labels'])
#logger.info("Tautulli DataFactory :: Updating metadata in the database for rating key: %s." % new_rating_key)
# logger.info("Tautulli DataFactory :: Updating metadata in the database for rating key: %s." % new_rating_key)
monitor_db = database.MonitorDatabase()
# Update the session_history_metadata table
@@ -1874,7 +1875,8 @@ class DataFactory(object):
query = 'SELECT * FROM recently_added WHERE rating_key = ?'
result = monitor_db.select(query=query, args=[rating_key])
except Exception as e:
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_recently_added_item: %s." % e)
logger.warn(
"Tautulli DataFactory :: Unable to execute database query for get_recently_added_item: %s." % e)
return []
else:
return []

View File

@@ -13,20 +13,11 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import object
import re
import jellypy
if jellypy.PYTHON2:
import database
import helpers
import logger
else:
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import database
from jellypy import helpers
from jellypy import logger
class DataTables(object):
@@ -224,7 +215,7 @@ class DataTables(object):
args.append('%' + search_param + '%')
if where:
where = 'WHERE ' + where.rstrip(' OR ')
return where, args
# This method extracts column data from our column list

View File

@@ -13,10 +13,8 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
class PlexPyException(Exception):
class JellyPyException(Exception):
"""
Generic Tautulli Exception - should never be thrown, only subclassed
"""

View File

@@ -14,36 +14,24 @@
#
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from backports import csv
import csv
import json
import os
import requests
import shutil
import threading
from functools import partial, reduce
from io import open
from multiprocessing.dummy import Pool as ThreadPool
import requests
import jellypy
if jellypy.PYTHON2:
import database
import datatables
import helpers
import logger
import users
from plex import Plex
else:
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import logger
from jellypy import users
from jellypy.plex import Plex
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import logger
from jellypy import users
from jellypy.plex import Plex
class Export(object):

View File

@@ -15,26 +15,15 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import range
from future.builtins import object
import datetime
import jellypy
if jellypy.PYTHON2:
import common
import database
import logger
import libraries
import session
else:
from jellypy import common
from jellypy import database
from jellypy import logger
from jellypy import libraries
from jellypy import session
from jellypy import common
from jellypy import database
from jellypy import logger
from jellypy import libraries
from jellypy import session
class Graphs(object):
@@ -224,7 +213,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_dayofweek: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_per_dayofweek: %s." % e)
return None
if jellypy.CONFIG.WEEK_START_MONDAY:
@@ -339,7 +329,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_hourofday: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_per_hourofday: %s." % e)
return None
hours_list = ['00', '01', '02', '03', '04', '05',
@@ -460,11 +451,11 @@ class Graphs(object):
dt_today = datetime.date.today()
dt = dt_today
month_range = [dt]
for n in range(int(time_range)-1):
if not ((dt_today.month-n) % 12)-1:
dt = datetime.date(dt.year-1, 12, 1)
for n in range(int(time_range) - 1):
if not ((dt_today.month - n) % 12) - 1:
dt = datetime.date(dt.year - 1, 12, 1)
else:
dt = datetime.date(dt.year, dt.month-1, 1)
dt = datetime.date(dt.year, dt.month - 1, 1)
month_range.append(dt)
categories = []
@@ -578,7 +569,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_platforms: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_platforms: %s." % e)
return None
categories = []
@@ -682,7 +674,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_users: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_users: %s." % e)
return None
categories = []
@@ -787,7 +780,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_stream_type: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_per_stream_type: %s." % e)
return None
# create our date range as some days may not have any data
@@ -893,7 +887,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_source_resolution: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_by_source_resolution: %s." % e)
return None
categories = []
@@ -954,7 +949,7 @@ class Graphs(object):
'THEN 1 ELSE 0 END) AS dp_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \
'THEN 1 ELSE 0 END) AS ds_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count, ' \
'COUNT(session_history.id) AS total_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
@@ -1003,7 +998,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_stream_resolution: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_total_plays_by_stream_resolution: %s." % e)
return None
categories = []
@@ -1092,7 +1088,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_platforms: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_platforms: %s." % e)
return None
categories = []
@@ -1190,7 +1187,8 @@ class Graphs(object):
result = monitor_db.select(query)
except Exception as e:
logger.warn("Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_users: %s." % e)
logger.warn(
"Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_users: %s." % e)
return None
categories = []

View File

@@ -15,28 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import unicode_literals
from future.builtins import zip
from future.builtins import str
import arrow
import base64
import cloudinary
from cloudinary.api import delete_resources_by_tag
from cloudinary.uploader import upload
from cloudinary.utils import cloudinary_url
from collections import OrderedDict
import datetime
from functools import reduce, wraps
import hashlib
import imghdr
from future.moves.itertools import islice, zip_longest
import ipwhois
import ipwhois.exceptions
import ipwhois.utils
from IPy import IP
import json
import math
import operator
@@ -48,21 +30,27 @@ import string
import sys
import time
import unicodedata
from future.moves.urllib.parse import urlencode
from collections import OrderedDict
from functools import reduce, wraps
from xml.dom import minidom
import arrow
import cloudinary
import ipwhois
import ipwhois.exceptions
import ipwhois.utils
import xmltodict
from IPy import IP
from cloudinary.api import delete_resources_by_tag
from cloudinary.uploader import upload
from cloudinary.utils import cloudinary_url
import jellypy
if jellypy.PYTHON2:
import common
import logger
import request
from api2 import API2
else:
from jellypy import common
from jellypy import logger
from jellypy import request
from jellypy.api2 import API2
from jellypy import common
from jellypy import logger
from jellypy import request
from jellypy.api2 import API2
def addtoapi(*dargs, **dkwargs):
@@ -77,6 +65,7 @@ def addtoapi(*dargs, **dkwargs):
@addtoapi()
"""
def rd(function):
@wraps(function)
def wrapper(*args, **kwargs):
@@ -107,7 +96,6 @@ def checked(variable):
def radio(variable, pos):
if variable == pos:
return 'Checked'
else:
@@ -161,7 +149,6 @@ def latinToAscii(unicrap, replace=False):
def convert_milliseconds(ms):
seconds = ms // 1000
gmtime = time.gmtime(seconds)
if seconds > 3600:
@@ -173,7 +160,6 @@ def convert_milliseconds(ms):
def convert_milliseconds_to_minutes(ms):
if str(ms).isdigit():
seconds = float(ms) / 1000
minutes = round(seconds / 60, 0)
@@ -184,7 +170,6 @@ def convert_milliseconds_to_minutes(ms):
def convert_seconds(s):
gmtime = time.gmtime(s)
if s > 3600:
minutes = time.strftime("%H:%M:%S", gmtime)
@@ -195,7 +180,6 @@ def convert_seconds(s):
def convert_seconds_to_minutes(s):
if str(s).isdigit():
minutes = round(float(s) / 60, 0)
@@ -302,7 +286,6 @@ def format_timedelta_Hms(td):
def get_age(date):
try:
split_date = date.split('-')
except:
@@ -317,7 +300,6 @@ def get_age(date):
def bytes_to_mb(bytes):
mb = float(bytes) / 1048576
size = '%.1f MB' % mb
return size
@@ -355,7 +337,6 @@ def piratesize(size):
def replace_all(text, dic, normalize=False):
if not text:
return ''
@@ -382,7 +363,6 @@ def replace_illegal_chars(string, type="file"):
def cleanName(string):
pass1 = latinToAscii(string).lower()
out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', pass1).encode('utf-8')
@@ -390,7 +370,6 @@ def cleanName(string):
def cleanTitle(title):
title = re.sub('[\.\-\/\_]', ' ', title).lower()
# Strip out extra whitespace
@@ -442,7 +421,8 @@ def split_path(f):
def extract_logline(s):
# Default log format
pattern = re.compile(r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)', re.VERBOSE)
pattern = re.compile(r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)',
re.VERBOSE)
match = pattern.match(s)
if match:
timestamp = match.group("timestamp")
@@ -526,7 +506,6 @@ def convert_xml_to_dict(xml):
def get_percent(value1, value2):
value1 = cast_to_float(value1)
value2 = cast_to_float(value2)
@@ -646,11 +625,14 @@ def sort_helper(k, sort_key, sort_keys):
def sanitize_out(*dargs, **dkwargs):
""" Helper decorator that sanitized the output
"""
def rd(function):
@wraps(function)
def wrapper(*args, **kwargs):
return sanitize(function(*args, **kwargs))
return wrapper
return rd
@@ -697,7 +679,6 @@ def is_valid_ip(address):
def whois_lookup(ip_address):
nets = []
err = None
try:
@@ -708,7 +689,7 @@ def whois_lookup(ip_address):
for net in nets:
net['country'] = countries.get(net['country'])
if net['postal_code']:
net['postal_code'] = net['postal_code'].replace('-', ' ')
net['postal_code'] = net['postal_code'].replace('-', ' ')
except ValueError as e:
err = 'Invalid IP address provided: %s.' % ip_address
except ipwhois.exceptions.IPDefinedError as e:
@@ -777,7 +758,8 @@ def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''):
delete_hash = imgur_response_data.get('deletehash', '')
else:
if err_msg:
logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Imgur: {}".format(img_title, fallback, err_msg))
logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Imgur: {}".format(img_title, fallback,
err_msg))
else:
logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Imgur.".format(img_title, fallback))
@@ -790,7 +772,8 @@ def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''):
def delete_from_imgur(delete_hash, img_title='', fallback=''):
""" Deletes an image from Imgur """
if not jellypy.CONFIG.IMGUR_CLIENT_ID:
logger.error("Tautulli Helpers :: Cannot delete image from Imgur. No Imgur client id specified in the settings.")
logger.error(
"Tautulli Helpers :: Cannot delete image from Imgur. No Imgur client id specified in the settings.")
return False
headers = {'Authorization': 'Client-ID %s' % jellypy.CONFIG.IMGUR_CLIENT_ID}
@@ -803,7 +786,9 @@ def delete_from_imgur(delete_hash, img_title='', fallback=''):
return True
else:
if err_msg:
logger.error("Tautulli Helpers :: Unable to delete image '{}' ({}) from Imgur: {}".format(img_title, fallback, err_msg))
logger.error(
"Tautulli Helpers :: Unable to delete image '{}' ({}) from Imgur: {}".format(img_title, fallback,
err_msg))
else:
logger.error("Tautulli Helpers :: Unable to delete image '{}' ({}) from Imgur.".format(img_title, fallback))
return False
@@ -814,7 +799,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
img_url = ''
if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET:
logger.error("Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.")
logger.error(
"Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.")
return img_url
cloudinary.config(
@@ -837,7 +823,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
logger.debug("Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback))
img_url = response.get('url', '')
except Exception as e:
logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Cloudinary: {}".format(img_title, fallback, e))
logger.error(
"Tautulli Helpers :: Unable to upload image '{}' ({}) to Cloudinary: {}".format(img_title, fallback, e))
return img_url
@@ -845,7 +832,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
def delete_from_cloudinary(rating_key=None, delete_all=False):
""" Deletes an image from Cloudinary """
if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET:
logger.error("Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.")
logger.error(
"Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.")
return False
cloudinary.config(
@@ -871,7 +859,8 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
url = ''
if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET:
logger.error("Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.")
logger.error(
"Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.")
return url
cloudinary.config(
@@ -903,7 +892,8 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options)
logger.debug("Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback))
except Exception as e:
logger.error("Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e))
logger.error(
"Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e))
return url
@@ -987,7 +977,7 @@ def human_file_size(bytes, si=True):
else:
return bytes
#thresh = 1000 if si else 1024
# thresh = 1000 if si else 1024
thresh = 1024 # Always divide by 2^10 but display SI units
if bytes < thresh:
return str(bytes) + ' B'
@@ -1064,7 +1054,7 @@ def parse_condition_logic_string(s, num_cond=0):
stack.pop()
nest_and -= 1
elif bool_next and x == 'and' and i < len(tokens)-1:
elif bool_next and x == 'and' and i < len(tokens) - 1:
stack[-1].append([])
stack.append(stack[-1][-1])
stack[-1].append(stack[-2].pop(-2))
@@ -1075,7 +1065,7 @@ def parse_condition_logic_string(s, num_cond=0):
close_bracket_next = False
nest_and += 1
elif bool_next and x == 'or' and i < len(tokens)-1:
elif bool_next and x == 'or' and i < len(tokens) - 1:
stack[-1].append(x)
cond_next = True
bool_next = False
@@ -1395,7 +1385,7 @@ def dict_merge(a, b, path=None):
return a
#https://stackoverflow.com/a/26853961
# https://stackoverflow.com/a/26853961
def dict_update(*dict_args):
"""
Given any number of dictionaries, shallow copy and merge into a new dict,

View File

@@ -14,25 +14,16 @@
#
# You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import object
from future.builtins import str
from multiprocessing.dummy import Pool as ThreadPool
from future.moves.urllib.parse import urljoin
from multiprocessing.pool import ThreadPool
from urllib.parse import urljoin
import certifi
import requests
import urllib3
import jellypy
if jellypy.PYTHON2:
import helpers
import logger
else:
from jellypy import helpers
from jellypy import logger
from jellypy import helpers
from jellypy import logger
class HTTPHandler(object):

View File

@@ -15,37 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import next
from future.builtins import object
import json
import os
import jellypy
if jellypy.PYTHON2:
import common
import database
import datatables
import helpers
import logger
import plextv
import pmsconnect
import session
import users
from plex import Plex
else:
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import logger
from jellypy import plextv
from jellypy import pmsconnect
from jellypy import session
from jellypy import users
from jellypy.plex import Plex
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import logger
from jellypy import plextv
from jellypy import pmsconnect
from jellypy import session
from jellypy import users
from jellypy.plex import Plex
def refresh_libraries():
@@ -378,9 +362,10 @@ class Libraries(object):
join_tables=['session_history_metadata',
'session_history',
'session_history_media_info'],
join_evals=[['session_history_metadata.section_id', 'library_sections.section_id'],
['session_history_metadata.id', 'session_history.id'],
['session_history_metadata.id', 'session_history_media_info.id']],
join_evals=[
['session_history_metadata.section_id', 'library_sections.section_id'],
['session_history_metadata.id', 'session_history.id'],
['session_history_metadata.id', 'session_history_media_info.id']],
kwargs=kwargs)
except Exception as e:
logger.warn("Tautulli Libraries :: Unable to execute database query for get_list: %s." % e)
@@ -452,7 +437,8 @@ class Libraries(object):
return dict
def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False, kwargs=None):
def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False,
kwargs=None):
default_return = {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
@@ -505,7 +491,8 @@ class Libraries(object):
'GROUP BY session_history.%s ' % (count_by, group_by)
result = monitor_db.select(query, args=[section_id])
except Exception as e:
logger.warn("Tautulli Libraries :: Unable to execute database query for get_datatables_media_info2: %s." % e)
logger.warn(
"Tautulli Libraries :: Unable to execute database query for get_datatables_media_info2: %s." % e)
return default_return
watched_list = {}
@@ -522,8 +509,8 @@ class Libraries(object):
rows = json.load(inFile)
library_count = len(rows)
except IOError as e:
#logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key)
#logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key)
# logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key)
# logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key)
pass
elif section_id:
try:
@@ -532,8 +519,8 @@ class Libraries(object):
rows = json.load(inFile)
library_count = len(rows)
except IOError as e:
#logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id)
#logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id)
# logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id)
# logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id)
pass
# If no cache was imported, get all library children items
@@ -594,7 +581,8 @@ class Libraries(object):
# Cache the media info to a json file
if rating_key:
try:
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key))
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR,
'media_info_%s-%s.json' % (section_id, rating_key))
with open(outFilePath, 'w') as outFile:
json.dump(rows, outFile)
except IOError as e:
@@ -622,14 +610,14 @@ class Libraries(object):
# Get datatables JSON data
if kwargs.get('json_data'):
json_data = helpers.process_json_kwargs(json_kwargs=kwargs.get('json_data'))
#print json_data
# print json_data
# Search results
search_value = json_data['search']['value'].lower()
if search_value:
searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] + ['title']
for row in rows:
for k,v in row.items():
for k, v in row.items():
if k in searchable_columns and search_value in v.lower():
results.append(row)
break
@@ -649,7 +637,9 @@ class Libraries(object):
elif sort_key in ('file_size', 'bitrate', 'added_at', 'last_played', 'play_count'):
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse)
elif sort_key == 'video_resolution':
results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse)
results = sorted(results,
key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')),
reverse=reverse)
else:
results = sorted(results, key=lambda k: k[sort_key].lower(), reverse=reverse)
@@ -692,14 +682,14 @@ class Libraries(object):
rows = []
# Import media info cache from json file
if rating_key:
#logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key)
# logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key)
try:
inFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key))
with open(inFilePath, 'r') as inFile:
rows = json.load(inFile)
except IOError as e:
#logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key)
#logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key)
# logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key)
# logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key)
pass
elif section_id:
logger.debug("Tautulli Libraries :: Getting file sizes for section_id %s." % section_id)
@@ -708,8 +698,8 @@ class Libraries(object):
with open(inFilePath, 'r') as inFile:
rows = json.load(inFile)
except IOError as e:
#logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id)
#logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id)
# logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id)
# logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id)
pass
# Get the total file size for each item
@@ -727,7 +717,7 @@ class Libraries(object):
media_info = media_part_info = {}
if 'media_info' in child_metadata and len(child_metadata['media_info']) > 0:
media_info = child_metadata['media_info'][0]
if 'parts' in media_info and len (media_info['parts']) > 0:
if 'parts' in media_info and len(media_info['parts']) > 0:
media_part_info = next((p for p in media_info['parts'] if p['selected']),
media_info['parts'][0])
@@ -742,22 +732,25 @@ class Libraries(object):
with open(outFilePath, 'w') as outFile:
json.dump(rows, outFile)
except IOError as e:
logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key)
logger.debug(
"Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key)
elif section_id:
try:
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s.json' % section_id)
with open(outFilePath, 'w') as outFile:
json.dump(rows, outFile)
except IOError as e:
logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for section_id %s." % section_id)
logger.debug(
"Tautulli Libraries :: Unable to create cache file with file sizes for section_id %s." % section_id)
if rating_key:
#logger.debug("Tautulli Libraries :: File sizes updated for rating_key %s." % rating_key)
# logger.debug("Tautulli Libraries :: File sizes updated for rating_key %s." % rating_key)
pass
elif section_id:
logger.debug("Tautulli Libraries :: File sizes updated for section_id %s." % section_id)
return True
def set_config(self, section_id=None, custom_thumb='', custom_art='',
do_notify=1, keep_history=1, do_notify_created=1):
if section_id:
@@ -856,8 +849,9 @@ class Libraries(object):
return library_details
else:
logger.warn("Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
% section_id)
logger.warn(
"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
% section_id)
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
refresh_libraries()
@@ -1016,36 +1010,36 @@ class Libraries(object):
result = []
for row in result:
if row['media_type'] == 'episode' and row['parent_thumb']:
thumb = row['parent_thumb']
elif row['media_type'] == 'episode':
thumb = row['grandparent_thumb']
else:
thumb = row['thumb']
if row['media_type'] == 'episode' and row['parent_thumb']:
thumb = row['parent_thumb']
elif row['media_type'] == 'episode':
thumb = row['grandparent_thumb']
else:
thumb = row['thumb']
recent_output = {'row_id': row['id'],
'media_type': row['media_type'],
'rating_key': row['rating_key'],
'parent_rating_key': row['parent_rating_key'],
'grandparent_rating_key': row['grandparent_rating_key'],
'title': row['title'],
'parent_title': row['parent_title'],
'grandparent_title': row['grandparent_title'],
'original_title': row['original_title'],
'thumb': thumb,
'media_index': row['media_index'],
'parent_media_index': row['parent_media_index'],
'year': row['year'],
'originally_available_at': row['originally_available_at'],
'live': row['live'],
'guid': row['guid'],
'time': row['started'],
'user': row['user'],
'section_id': row['section_id'],
'content_rating': row['content_rating'],
'labels': row['labels'].split(';') if row['labels'] else (),
}
recently_watched.append(recent_output)
recent_output = {'row_id': row['id'],
'media_type': row['media_type'],
'rating_key': row['rating_key'],
'parent_rating_key': row['parent_rating_key'],
'grandparent_rating_key': row['grandparent_rating_key'],
'title': row['title'],
'parent_title': row['parent_title'],
'grandparent_title': row['grandparent_title'],
'original_title': row['original_title'],
'thumb': thumb,
'media_index': row['media_index'],
'parent_media_index': row['parent_media_index'],
'year': row['year'],
'originally_available_at': row['originally_available_at'],
'live': row['live'],
'guid': row['guid'],
'time': row['started'],
'user': row['user'],
'section_id': row['section_id'],
'content_rating': row['content_rating'],
'labels': row['labels'].split(';') if row['labels'] else (),
}
recently_watched.append(recent_output)
return session.mask_session_info(recently_watched)

View File

@@ -14,19 +14,11 @@
#
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import object
import future.moves.queue as queue
import time
import queue
import threading
import time
import jellypy
if jellypy.PYTHON2:
import logger
else:
from jellypy import logger
from jellypy import logger
class TimedLock(object):

View File

@@ -15,22 +15,15 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import os
from io import open
import os
import jellypy
if jellypy.PYTHON2:
import helpers
import logger
else:
from jellypy import helpers
from jellypy import logger
from jellypy import helpers
from jellypy import logger
def get_log_tail(window=20, parsed=True, log_type="server"):
if jellypy.CONFIG.PMS_LOGS_FOLDER:
log_file = ""
if log_type == "server":
@@ -76,6 +69,7 @@ def get_log_tail(window=20, parsed=True, log_type="server"):
return log_lines
# http://stackoverflow.com/a/13790289/2405162
def tail(f, lines=1, _buffer=4098):
"""Tail a file and get X lines from the end"""
@@ -105,4 +99,4 @@ def tail(f, lines=1, _buffer=4098):
# next X bytes
block_counter -= 1
return lines_found[-lines:]
return lines_found[-lines:]

View File

@@ -15,13 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from logutils.queue import QueueHandler, QueueListener
from logging import handlers
import cherrypy
import contextlib
import errno
import logging
@@ -31,15 +25,15 @@ import re
import sys
import threading
import traceback
from logging import handlers
import cherrypy
from logutils.queue import QueueHandler, QueueListener
import jellypy
if jellypy.PYTHON2:
import helpers
from config import _BLACKLIST_KEYS, _WHITELIST_KEYS
else:
from jellypy import helpers
from jellypy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
from jellypy import helpers
from jellypy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
# These settings are for file logging only
FILENAME = "tautulli.log"
@@ -67,8 +61,8 @@ def blacklist_config(config):
for key, value in config.items():
if isinstance(value, str) and len(value.strip()) > 5 and \
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
blacklist.add(value.strip())
_BLACKLIST_WORDS.update(blacklist)
@@ -78,9 +72,10 @@ class NoThreadFilter(logging.Filter):
"""
Log filter for the current thread
"""
def __init__(self, threadName):
super(NoThreadFilter, self).__init__()
self.threadName = threadName
def filter(self, record):
@@ -92,6 +87,7 @@ class BlacklistFilter(logging.Filter):
"""
Log filter for blacklisted tokens and passwords
"""
def __init__(self):
super(BlacklistFilter, self).__init__()
@@ -125,6 +121,7 @@ class RegexFilter(logging.Filter):
"""
Base class for regex log filter
"""
def __init__(self):
super(RegexFilter, self).__init__()
@@ -165,6 +162,7 @@ class PublicIPFilter(RegexFilter):
"""
Log filter for public IP addresses
"""
def __init__(self):
super(PublicIPFilter, self).__init__()
@@ -182,6 +180,7 @@ class EmailFilter(RegexFilter):
"""
Log filter for email addresses
"""
def __init__(self):
super(EmailFilter, self).__init__()
@@ -198,6 +197,7 @@ class PlexTokenFilter(RegexFilter):
"""
Log filter for X-Plex-Token
"""
def __init__(self):
super(PlexTokenFilter, self).__init__()
@@ -309,11 +309,13 @@ def initLogger(console=False, log_dir=False, verbose=False):
# Setup file logger
if log_dir:
file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%Y-%m-%d %H:%M:%S')
file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s',
'%Y-%m-%d %H:%M:%S')
# Main Tautulli logger
filename = os.path.join(log_dir, FILENAME)
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8')
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES,
encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
@@ -322,7 +324,8 @@ def initLogger(console=False, log_dir=False, verbose=False):
# Tautulli API logger
filename = os.path.join(log_dir, FILENAME_API)
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8')
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES,
encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
@@ -330,7 +333,8 @@ def initLogger(console=False, log_dir=False, verbose=False):
# Tautulli websocket logger
filename = os.path.join(log_dir, FILENAME_PLEX_WEBSOCKET)
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8')
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES,
encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(file_formatter)
@@ -338,7 +342,8 @@ def initLogger(console=False, log_dir=False, verbose=False):
# Setup console logger
if console:
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%Y-%m-%d %H:%M:%S')
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s',
'%Y-%m-%d %H:%M:%S')
console_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter)
console_handler.setLevel(logging.DEBUG)
@@ -407,6 +412,7 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
raise
except:
excepthook(*sys.exc_info())
self.run = new_run
# Monkey patch the run() by monkey patching the __init__ method

View File

@@ -16,13 +16,14 @@
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import os
import plistlib
import subprocess
import sys
import plistlib
try:
import AppKit
import Foundation
HAS_PYOBJC = True
except ImportError:
HAS_PYOBJC = False
@@ -31,14 +32,10 @@ if HAS_PYOBJC:
import rumps
import jellypy
if jellypy.PYTHON2:
import common
import logger
import versioncheck
else:
from jellypy import common
from jellypy import logger
from jellypy import versioncheck
from jellypy import common
from jellypy import logger
from jellypy import versioncheck
class MacOSSystemTray(object):

View File

@@ -15,22 +15,13 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
import requests
import threading
import jellypy
if jellypy.PYTHON2:
import database
import helpers
import logger
else:
from jellypy import database
from jellypy import helpers
from jellypy import logger
import requests
from jellypy import database
from jellypy import helpers
from jellypy import logger
TEMP_DEVICE_TOKEN = None
INVALIDATE_TIMER = None
@@ -118,7 +109,8 @@ def get_mobile_device_config(mobile_device_id=None):
if str(mobile_device_id).isdigit():
mobile_device_id = int(mobile_device_id)
else:
logger.error("Tautulli MobileApp :: Unable to retrieve mobile device config: invalid mobile_device_id %s." % mobile_device_id)
logger.error(
"Tautulli MobileApp :: Unable to retrieve mobile device config: invalid mobile_device_id %s." % mobile_device_id)
return None
db = database.MonitorDatabase()
@@ -132,7 +124,8 @@ def set_mobile_device_config(mobile_device_id=None, **kwargs):
if str(mobile_device_id).isdigit():
mobile_device_id = int(mobile_device_id)
else:
logger.error("Tautulli MobileApp :: Unable to set exisiting mobile device: invalid mobile_device_id %s." % mobile_device_id)
logger.error(
"Tautulli MobileApp :: Unable to set exisiting mobile device: invalid mobile_device_id %s." % mobile_device_id)
return False
keys = {'id': mobile_device_id}

View File

@@ -15,26 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from io import open
import email.utils
import os
from io import open
from apscheduler.triggers.cron import CronTrigger
import email.utils
import jellypy
if jellypy.PYTHON2:
import database
import helpers
import logger
import newsletters
else:
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import newsletters
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import newsletters
NEWSLETTER_SCHED = None
@@ -59,7 +50,8 @@ def schedule_newsletters(newsletter_id=None):
if newsletter['active']:
schedule_newsletter_job('newsletter-{}'.format(newsletter['id']), name=newsletter_job_name,
func=add_newsletter_each, args=[newsletter['id'], 'on_cron'], cron=newsletter['cron'])
func=add_newsletter_each, args=[newsletter['id'], 'on_cron'],
cron=newsletter['cron'])
else:
schedule_newsletter_job('newsletter-{}'.format(newsletter['id']), name=newsletter_job_name,
remove_job=True)
@@ -143,7 +135,6 @@ def notify(newsletter_id=None, notify_action=None, **kwargs):
def set_notify_state(newsletter, notify_action, subject, body, message, filename,
start_date, end_date, start_time, end_time, newsletter_uuid, email_msg_id):
if newsletter and notify_action:
db = database.MonitorDatabase()
@@ -220,6 +211,7 @@ def get_newsletter(newsletter_uuid=None, newsletter_id_name=None):
newsletter = n_file.read()
return newsletter
except OSError as e:
logger.error("Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e))
logger.error(
"Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e))
else:
logger.warn("Tautulli NewsletterHandler :: Newsletter file '%s' is missing." % newsletter_file)

View File

@@ -15,40 +15,25 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
from future.builtins import object
import arrow
from collections import OrderedDict
import json
from itertools import groupby
from mako.lookup import TemplateLookup
from mako import exceptions
import os
import re
from collections import OrderedDict
from itertools import groupby
import arrow
from mako import exceptions
from mako.lookup import TemplateLookup
import jellypy
if jellypy.PYTHON2:
import common
import database
import helpers
import libraries
import logger
import newsletter_handler
import pmsconnect
from notifiers import send_notification, EMAIL
else:
from jellypy import common
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import newsletter_handler
from jellypy import pmsconnect
from jellypy.notifiers import send_notification, EMAIL
from jellypy import common
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import newsletter_handler
from jellypy import pmsconnect
from jellypy.notifiers import send_notification, EMAIL
AGENT_IDS = {
'recently_added': 0
@@ -512,7 +497,8 @@ class Newsletter(object):
self.newsletter = self.generate_newsletter()
if self.template_error:
logger.error("Tautulli Newsletters :: %s newsletter failed to render template. Newsletter not sent." % self.NAME)
logger.error(
"Tautulli Newsletters :: %s newsletter failed to render template. Newsletter not sent." % self.NAME)
return False
if not self._has_data():
@@ -540,7 +526,7 @@ class Newsletter(object):
for line in self.newsletter.splitlines():
if '<!-- IGNORE SAVE -->' not in line:
n_file.write((line + '\r\n').encode('utf-8'))
#n_file.write(line.strip())
# n_file.write(line.strip())
logger.info("Tautulli Newsletters :: %s newsletter saved to '%s'" % (self.NAME, newsletter_file))
except OSError as e:
@@ -578,10 +564,10 @@ class Newsletter(object):
)
elif self.config['notifier_id']:
return send_notification(
notifier_id=self.config['notifier_id'],
subject=self.subject_formatted,
body=self.body_formatted
)
notifier_id=self.config['notifier_id'],
subject=self.subject_formatted,
body=self.body_formatted
)
def build_params(self):
parameters = self._build_params()
@@ -628,7 +614,8 @@ class Newsletter(object):
try:
subject = custom_formatter.format(str(self.subject), **self.parameters)
except LookupError as e:
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter subject. Using fallback." % e)
logger.error(
"Tautulli Newsletter :: Unable to parse parameter %s in newsletter subject. Using fallback." % e)
subject = str(self._DEFAULT_SUBJECT).format(**self.parameters)
except Exception as e:
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
@@ -646,7 +633,8 @@ class Newsletter(object):
try:
message = custom_formatter.format(str(self.message), **self.parameters)
except LookupError as e:
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter message. Using fallback." % e)
logger.error(
"Tautulli Newsletter :: Unable to parse parameter %s in newsletter message. Using fallback." % e)
message = str(self._DEFAULT_MESSAGE).format(**self.parameters)
except Exception as e:
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter message: %s. Using fallback." % e)
@@ -661,7 +649,8 @@ class Newsletter(object):
try:
filename = custom_formatter.format(str(self.filename), **self.parameters)
except LookupError as e:
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e)
logger.error(
"Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e)
filename = str(self._DEFAULT_FILENAME).format(**self.parameters)
except Exception as e:
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
@@ -821,7 +810,8 @@ class RecentlyAdded(Newsletter):
from jellypy.notification_handler import get_img_info, set_hash_image_info
if not self.config['incl_libraries']:
logger.warn("Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME)
logger.warn(
"Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME)
media_types = set()
for s in self._get_sections():

View File

@@ -15,60 +15,40 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import map
from future.builtins import str
from future.builtins import range
import hashlib
import json
import os
import re
import threading
import time
from collections import Counter, defaultdict
from functools import partial
from itertools import groupby
from operator import itemgetter
from string import Formatter
import arrow
import bleach
from collections import Counter, defaultdict
from functools import partial
import hashlib
from itertools import groupby
import json
from operator import itemgetter
import os
import re
from string import Formatter
import threading
import time
import musicbrainzngs
import jellypy
if jellypy.PYTHON2:
import activity_processor
import common
import database
import datafactory
import logger
import helpers
import notifiers
import pmsconnect
import request
from newsletter_handler import notify as notify_newsletter
else:
from jellypy import activity_processor
from jellypy import common
from jellypy import database
from jellypy import datafactory
from jellypy import logger
from jellypy import helpers
from jellypy import notifiers
from jellypy import pmsconnect
from jellypy import request
from jellypy.newsletter_handler import notify as notify_newsletter
from jellypy import activity_processor
from jellypy import common
from jellypy import database
from jellypy import datafactory
from jellypy import logger
from jellypy import helpers
from jellypy import notifiers
from jellypy import pmsconnect
from jellypy import request
from jellypy.newsletter_handler import notify as notify_newsletter
def process_queue():
queue = jellypy.NOTIFY_QUEUE
while True:
params = queue.get()
if params is None:
break
elif params:
@@ -88,14 +68,16 @@ def process_queue():
def start_threads(num_threads=1):
logger.info("Tautulli NotificationHandler :: Starting background notification handler ({} threads).".format(num_threads))
logger.info(
"Tautulli NotificationHandler :: Starting background notification handler ({} threads).".format(num_threads))
for x in range(num_threads):
thread = threading.Thread(target=process_queue)
thread.daemon = True
thread.start()
def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, manual_trigger=False, **kwargs):
def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, manual_trigger=False,
**kwargs):
if not notify_action:
logger.debug("Tautulli NotificationHandler :: Notify called but no action received.")
return
@@ -119,7 +101,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
if notifiers_enabled and (manual_trigger or conditions):
if manual_trigger:
logger.debug("Tautulli NotificationHandler :: Notifiers enabled for notify_action '%s' (manual trigger)." % notify_action)
logger.debug(
"Tautulli NotificationHandler :: Notifiers enabled for notify_action '%s' (manual trigger)." % notify_action)
if stream_data or timeline_data:
# Build the notification parameters
@@ -150,7 +133,9 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
data.update(kwargs)
jellypy.NOTIFY_QUEUE.put(data)
else:
logger.debug("Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % notifier['id'])
logger.debug(
"Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." %
notifier['id'])
# Add on_concurrent and on_newdevice to queue if action is on_play
if notify_action == 'on_play':
@@ -187,7 +172,8 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]
if jellypy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
evaluated = len(
Counter(s['ip_address'] for s in user_sessions)) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
else:
evaluated = len(user_sessions) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
@@ -202,8 +188,10 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
if notify_action == 'on_stop':
evaluated = (jellypy.CONFIG.NOTIFY_CONSECUTIVE or
(stream_data['media_type'] == 'movie' and progress_percent < jellypy.CONFIG.MOVIE_WATCHED_PERCENT) or
(stream_data['media_type'] == 'episode' and progress_percent < jellypy.CONFIG.TV_WATCHED_PERCENT))
(stream_data[
'media_type'] == 'movie' and progress_percent < jellypy.CONFIG.MOVIE_WATCHED_PERCENT) or
(stream_data[
'media_type'] == 'episode' and progress_percent < jellypy.CONFIG.TV_WATCHED_PERCENT))
elif notify_action == 'on_resume':
evaluated = jellypy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
@@ -218,7 +206,8 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
else:
evaluated = False
logger.debug("Tautulli NotificationHandler :: Global notification conditions evaluated to '{}'.".format(evaluated))
logger.debug(
"Tautulli NotificationHandler :: Global notification conditions evaluated to '{}'.".format(evaluated))
# Recently Added notifications
elif timeline_data:
@@ -272,7 +261,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
if not parameter or not operator or not values:
evaluated = True
evaluated_conditions.append(evaluated)
logger.debug("Tautulli NotificationHandler :: {%s} Blank condition > %s" % (i+1, evaluated))
logger.debug("Tautulli NotificationHandler :: {%s} Blank condition > %s" % (i + 1, evaluated))
continue
# Make sure the condition values is in a list
@@ -291,8 +280,9 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
values = [helpers.cast_to_float(v) for v in values]
except ValueError as e:
logger.error("Tautulli NotificationHandler :: {%s} Unable to cast condition '%s', values '%s', to type '%s'."
% (i+1, parameter, values, parameter_type))
logger.error(
"Tautulli NotificationHandler :: {%s} Unable to cast condition '%s', values '%s', to type '%s'."
% (i + 1, parameter, values, parameter_type))
return False
# Cast the parameter value to the correct type
@@ -307,8 +297,9 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
parameter_value = helpers.cast_to_float(parameter_value)
except ValueError as e:
logger.error("Tautulli NotificationHandler :: {%s} Unable to cast parameter '%s', value '%s', to type '%s'."
% (i+1, parameter, parameter_value, parameter_type))
logger.error(
"Tautulli NotificationHandler :: {%s} Unable to cast parameter '%s', value '%s', to type '%s'."
% (i + 1, parameter, parameter_value, parameter_type))
return False
# Check each condition
@@ -339,11 +330,12 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
else:
evaluated = None
logger.warn("Tautulli NotificationHandler :: {%s} Invalid condition operator '%s' > %s."
% (i+1, operator, evaluated))
% (i + 1, operator, evaluated))
evaluated_conditions.append(evaluated)
logger.debug("Tautulli NotificationHandler :: {%s} %s | %s | %s > '%s' > %s"
% (i+1, parameter, operator, ' or '.join(["'%s'" % v for v in values]), parameter_value, evaluated))
% (i + 1, parameter, operator, ' or '.join(["'%s'" % v for v in values]), parameter_value,
evaluated))
if logic_groups:
# Format and evaluate the logic string
@@ -357,7 +349,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
else:
evaluated_logic = all(evaluated_conditions[1:])
logger.debug("Tautulli NotificationHandler :: Condition logic [blank]: %s > %s"
% (' and '.join(['{%s}' % (i+1) for i in range(len(custom_conditions))]), evaluated_logic))
% (' and '.join(['{%s}' % (i + 1) for i in range(len(custom_conditions))]), evaluated_logic))
logger.debug("Tautulli NotificationHandler :: Custom conditions evaluated to '{}'. Conditions: {}.".format(
evaluated_logic, evaluated_conditions[1:]))
@@ -457,7 +449,6 @@ def get_notify_state_enabled(session, notify_action, notified=True):
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
if notifier and notify_action:
monitor_db = database.MonitorDatabase()
@@ -571,7 +562,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
total_bandwidth = lan_bandwidth + wan_bandwidth
# Generate a combined transcode decision value
if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode':
if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision',
'') == 'transcode':
transcode_decision = 'Transcode'
elif session.get('stream_video_decision', '') == 'copy' or session.get('stream_audio_decision', '') == 'copy':
transcode_decision = 'Direct Stream'
@@ -597,7 +589,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
else:
plex_web_rating_key = notify_params['rating_key']
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
notify_params[
'plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
web_url=jellypy.CONFIG.PMS_WEB_URL,
pms_identifier=jellypy.CONFIG.PMS_IDENTIFIER,
rating_key=plex_web_rating_key)
@@ -621,7 +614,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
if 'thetvdb://' in notify_params['guid'] or notify_params['thetvdb_id']:
notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or \
notify_params['guid'].split('thetvdb://')[1].split('/')[0].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'] + '?type=show'
@@ -632,12 +626,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
if 'themoviedb://' in notify_params['guid'] or notify_params['themoviedb_id']:
if notify_params['media_type'] == 'movie':
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('?')[0]
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or \
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'] + '?type=movie'
notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params[
'themoviedb_id'] + '?type=movie'
elif notify_params['media_type'] in ('show', 'season', 'episode'):
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0]
notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or \
notify_params['guid'].split('themoviedb://')[1].split('/')[0].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'] + '?type=show'
@@ -710,7 +707,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
if tvmaze_info.get('thetvdb_id'):
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id'])
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(notify_params['thetvdb_id']) + '?type=show'
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(
notify_params['thetvdb_id']) + '?type=show'
if tvmaze_info.get('imdb_id'):
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
@@ -779,7 +777,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
notify_params.update(poster_info)
if ((manual_trigger or jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT)
and notify_params['media_type'] in ('show', 'artist')):
and notify_params['media_type'] in ('show', 'artist')):
show_name = notify_params['title']
episode_name = ''
artist_name = notify_params['title']
@@ -999,15 +997,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'track_count': grandchild_count,
'year': notify_params['year'],
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
if notify_params['originally_available_at'] else '',
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 '',
if notify_params['originally_available_at'] else '',
'added_date': arrow.get(notify_params['added_at']).format(date_format)
if notify_params['added_at'] else '',
if notify_params['added_at'] else '',
'updated_date': arrow.get(notify_params['updated_at']).format(date_format)
if notify_params['updated_at'] else '',
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 '',
if notify_params['last_viewed_at'] else '',
'studio': notify_params['studio'],
'content_rating': notify_params['content_rating'],
'directors': ', '.join(notify_params['directors']),
@@ -1019,7 +1017,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'summary': notify_params['summary'],
'tagline': notify_params['tagline'],
'rating': rating,
'critic_rating': critic_rating,
'critic_rating': critic_rating,
'audience_rating': audience_rating,
'user_rating': notify_params['user_rating'],
'duration': duration,
@@ -1089,7 +1087,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'parent_thumb': notify_params['parent_thumb'],
'grandparent_thumb': notify_params['grandparent_thumb'],
'poster_thumb': poster_thumb
}
}
return available_params
@@ -1147,7 +1145,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
'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 '',
if pms_download_info['release_date'] else '',
'update_channel': 'Beta' if update_channel == 'beta' else 'Public',
'update_platform': pms_download_info['platform'],
'update_distro': pms_download_info['distro'],
@@ -1164,7 +1162,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''),
'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''),
'tautulli_update_changelog': plexpy_download_info['body']
}
}
return available_params
@@ -1189,8 +1187,8 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
media_type = parameters.get('media_type')
all_tags = r'<movie>.*?</movie>|' \
'<show>.*?</show>|<season>.*?</season>|<episode>.*?</episode>|' \
'<artist>.*?</artist>|<album>.*?</album>|<track>.*?</track>'
'<show>.*?</show>|<season>.*?</season>|<episode>.*?</episode>|' \
'<artist>.*?</artist>|<album>.*?</album>|<track>.*?</track>'
# Check for exclusion tags
if media_type == 'movie':
@@ -1200,7 +1198,8 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
elif media_type == 'season':
pattern = re.compile(all_tags.replace('<season>.*?</season>', '<season>|</season>'), re.IGNORECASE | re.DOTALL)
elif media_type == 'episode':
pattern = re.compile(all_tags.replace('<episode>.*?</episode>', '<episode>|</episode>'), re.IGNORECASE | re.DOTALL)
pattern = re.compile(all_tags.replace('<episode>.*?</episode>', '<episode>|</episode>'),
re.IGNORECASE | re.DOTALL)
elif media_type == 'artist':
pattern = re.compile(all_tags.replace('<artist>.*?</artist>', '<artist>|</artist>'), re.IGNORECASE | re.DOTALL)
elif media_type == 'album':
@@ -1224,10 +1223,12 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
try:
script_args = [str_formatter(arg) for arg in helpers.split_args(subject)]
except LookupError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
script_args = []
except Exception as e:
logger.exception("Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
logger.exception(
"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
script_args = []
elif agent_id == 25:
@@ -1235,51 +1236,61 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
try:
subject = json.loads(subject)
except ValueError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse custom webhook json header data: %s. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse custom webhook json header data: %s. Using fallback." % e)
subject = ''
if subject:
try:
subject = json.dumps(helpers.traverse_map(subject, str_formatter))
except LookupError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in webhook header data. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse parameter %s in webhook header data. Using fallback." % e)
subject = ''
except Exception as e:
logger.exception("Tautulli NotificationHandler :: Unable to parse custom webhook header data: %s. Using fallback." % e)
logger.exception(
"Tautulli NotificationHandler :: Unable to parse custom webhook header data: %s. Using fallback." % e)
subject = ''
if body:
try:
body = json.loads(body)
except ValueError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse custom webhook json body data: %s. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse custom webhook json body data: %s. Using fallback." % e)
body = ''
if body:
try:
body = json.dumps(helpers.traverse_map(body, str_formatter))
except LookupError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in webhook body data. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse parameter %s in webhook body data. Using fallback." % e)
body = ''
except Exception as e:
logger.exception("Tautulli NotificationHandler :: Unable to parse custom webhook body data: %s. Using fallback." % e)
logger.exception(
"Tautulli NotificationHandler :: Unable to parse custom webhook body data: %s. Using fallback." % e)
body = ''
else:
try:
subject = str_formatter(subject)
except LookupError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
subject = str(default_subject).format(**parameters)
except Exception as e:
logger.exception("Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
logger.exception(
"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
subject = str(default_subject).format(**parameters)
try:
body = str_formatter(body)
except LookupError as e:
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
logger.error(
"Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
body = str(default_body).format(**parameters)
except Exception as e:
logger.exception("Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
logger.exception(
"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
body = str(default_body).format(**parameters)
return subject, body, script_args
@@ -1325,7 +1336,7 @@ def format_group_index(group_keys):
num = []
num00 = []
for k, g in groupby(enumerate(group_keys), lambda i_x: i_x[0]-i_x[1]):
for k, g in groupby(enumerate(group_keys), lambda i_x: i_x[0] - i_x[1]):
group = list(map(itemgetter(1), g))
g_min, g_max = min(group), max(group)
@@ -1499,14 +1510,16 @@ def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, title=No
'WHERE rating_key = ?'
tvmaze_info = db.select_single(query, args=[rating_key])
except Exception as e:
logger.warn("Tautulli NotificationHandler :: Unable to execute database query for lookup_tvmaze_by_tvdb_id: %s." % e)
logger.warn(
"Tautulli NotificationHandler :: Unable to execute database query for lookup_tvmaze_by_tvdb_id: %s." % e)
return {}
if not tvmaze_info:
tvmaze_info = {}
if thetvdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for thetvdb_id '{}'.".format(thetvdb_id))
logger.debug(
"Tautulli NotificationHandler :: Looking up TVmaze info for thetvdb_id '{}'.".format(thetvdb_id))
elif imdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for imdb_id '{}'.".format(imdb_id))
else:
@@ -1559,18 +1572,23 @@ def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, titl
'WHERE rating_key = ?'
themoviedb_info = db.select_single(query, args=[rating_key])
except Exception as e:
logger.warn("Tautulli NotificationHandler :: Unable to execute database query for lookup_themoviedb_by_imdb_id: %s." % e)
logger.warn(
"Tautulli NotificationHandler :: Unable to execute database query for lookup_themoviedb_by_imdb_id: %s." % e)
return {}
if not themoviedb_info:
themoviedb_info = {}
if thetvdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for thetvdb_id '{}'.".format(thetvdb_id))
logger.debug(
"Tautulli NotificationHandler :: Looking up The Movie Database info for thetvdb_id '{}'.".format(
thetvdb_id))
elif imdb_id:
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for imdb_id '{}'.".format(imdb_id))
logger.debug(
"Tautulli NotificationHandler :: Looking up The Movie Database info for imdb_id '{}'.".format(imdb_id))
else:
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year))
logger.debug(
"Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year))
params = {'api_key': jellypy.CONFIG.THEMOVIEDB_APIKEY}
@@ -1648,10 +1666,12 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
themoviedb_json = {}
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format(themoviedb_id))
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format(
themoviedb_id))
params = {'api_key': jellypy.CONFIG.THEMOVIEDB_APIKEY}
response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params)
response, err_msg, req_msg = request.request_response2(
'https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params)
if response and not err_msg:
themoviedb_json = response.json()
@@ -1876,7 +1896,7 @@ class CustomFormatter(Formatter):
else:
format_spec, auto_arg_index = self._vformat(
format_spec, args, kwargs,
used_args, recursion_depth-1,
used_args, recursion_depth - 1,
auto_arg_index=auto_arg_index)
# format the object and append to the result

View File

@@ -15,9 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import object
import base64
import bleach
@@ -34,8 +31,6 @@ import subprocess
import sys
import threading
import time
from future.moves.urllib.parse import urlencode
from future.moves.urllib.parse import urlparse
try:
from Cryptodome.Protocol.KDF import PBKDF2

View File

@@ -16,8 +16,6 @@
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import object
from future.builtins import str
from plexapi.server import PlexServer

View File

@@ -15,26 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
import arrow
import sqlite3
from xml.dom import minidom
import jellypy
if jellypy.PYTHON2:
import activity_processor
import database
import helpers
import logger
import users
else:
from jellypy import activity_processor
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import users
import arrow
from jellypy import activity_processor
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import users
def extract_plexivity_xml(xml=None):
@@ -274,7 +264,6 @@ def validate_database(database_file=None, table_name=None):
def import_from_plexivity(database_file=None, table_name=None, import_ignore_interval=0):
try:
connection = sqlite3.connect(database_file, timeout=20)
connection.row_factory = sqlite3.Row
@@ -339,7 +328,7 @@ def import_from_plexivity(database_file=None, table_name=None, import_ignore_int
continue
# Skip line if we don't have a ratingKey to work with
#if not row['rating_key']:
# if not row['rating_key']:
# logger.error("Tautulli Importer :: Skipping record due to null ratingKey.")
# continue

View File

@@ -15,31 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
from future.builtins import object
import base64
import json
import jellypy
if jellypy.PYTHON2:
import common
import helpers
import http_handler
import logger
import users
import pmsconnect
import session
else:
from jellypy import common
from jellypy import helpers
from jellypy import http_handler
from jellypy import logger
from jellypy import users
from jellypy import pmsconnect
from jellypy import session
from jellypy import common
from jellypy import helpers
from jellypy import http_handler
from jellypy import logger
from jellypy import users
from jellypy import pmsconnect
from jellypy import session
def get_server_resources(return_presence=False, return_server=False, return_info=False, **kwargs):
@@ -212,7 +198,8 @@ class PlexTV(object):
if force:
logger.debug("Tautulli PlexTV :: Forcing refresh of Plex.tv token.")
devices_list = self.get_devices_list()
device_id = next((d for d in devices_list if d['device_identifier'] == jellypy.CONFIG.PMS_UUID), {}).get('device_id', None)
device_id = next((d for d in devices_list if d['device_identifier'] == jellypy.CONFIG.PMS_UUID), {}).get(
'device_id', None)
if device_id:
logger.debug("Tautulli PlexTV :: Removing Tautulli from Plex.tv devices.")
@@ -233,7 +220,6 @@ class PlexTV(object):
logger.info("Tautulli PlexTV :: Updated Plex.tv token for Tautulli.")
return token
def get_server_token(self):
servers = self.get_plextv_resources(output_format='xml')
server_token = ''
@@ -794,7 +780,8 @@ class PlexTV(object):
'port': helpers.get_xml_attr(c, 'port'),
'uri': helpers.get_xml_attr(c, 'uri'),
'local': helpers.get_xml_attr(c, 'local'),
'value': helpers.get_xml_attr(c, 'address') + ':' + helpers.get_xml_attr(c, 'port'),
'value': helpers.get_xml_attr(c, 'address') + ':' + helpers.get_xml_attr(c,
'port'),
'is_cloud': is_cloud
}
clean_servers.append(server)
@@ -826,15 +813,17 @@ class PlexTV(object):
# Get the updates for the platform
pms_platform = common.PMS_PLATFORM_NAME_OVERRIDES.get(jellypy.CONFIG.PMS_PLATFORM, jellypy.CONFIG.PMS_PLATFORM)
platform_downloads = available_downloads.get('computer').get(pms_platform) or \
available_downloads.get('nas').get(pms_platform)
available_downloads.get('nas').get(pms_platform)
if not platform_downloads:
logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Could not match server platform: %s."
% pms_platform)
return {}
v_old = helpers.cast_to_int("".join(v.zfill(4) for v in jellypy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4]))
v_new = helpers.cast_to_int("".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4]))
v_old = helpers.cast_to_int(
"".join(v.zfill(4) for v in jellypy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4]))
v_new = helpers.cast_to_int(
"".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4]))
if not v_old:
logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid current server version: %s."
@@ -983,7 +972,7 @@ class PlexTV(object):
"latitude": latitude,
"longitude": longitude,
"continent": None, # keep for backwards compatibility with GeoLite2
"accuracy": None # keep for backwards compatibility with GeoLite2
"accuracy": None # keep for backwards compatibility with GeoLite2
}
return geo_info

View File

@@ -15,25 +15,14 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
import sqlite3
from xml.dom import minidom
import jellypy
if jellypy.PYTHON2:
import activity_processor
import database
import helpers
import logger
import users
else:
from jellypy import activity_processor
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import users
from jellypy import activity_processor
from jellypy import database
from jellypy import helpers
from jellypy import logger
from jellypy import users
def extract_plexwatch_xml(xml=None):
@@ -265,7 +254,6 @@ def validate_database(database_file=None, table_name=None):
def import_from_plexwatch(database_file=None, table_name=None, import_ignore_interval=0):
try:
connection = sqlite3.connect(database_file, timeout=20)
connection.row_factory = sqlite3.Row

View File

@@ -15,38 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
from future.builtins import object
import json
import os
import time
from future.moves.urllib.parse import quote, quote_plus, urlencode
from urllib.parse import quote_plus, quote, urlencode
from xml.dom.minidom import Node
import jellypy
if jellypy.PYTHON2:
import activity_processor
import common
import helpers
import http_handler
import libraries
import logger
import plextv
import session
import users
else:
from jellypy import activity_processor
from jellypy import common
from jellypy import helpers
from jellypy import http_handler
from jellypy import libraries
from jellypy import logger
from jellypy import plextv
from jellypy import session
from jellypy import users
from jellypy import activity_processor
from jellypy import common
from jellypy import helpers
from jellypy import http_handler
from jellypy import libraries
from jellypy import logger
from jellypy import plextv
from jellypy import session
from jellypy import users
def get_server_friendly_name():
@@ -217,7 +200,8 @@ class PmsConnect(object):
Output: array
"""
uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (section_id, start, count)
uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (
section_id, start, count)
request = self.request_handler.make_request(uri=uri,
request_type='GET',
output_format=output_format)
@@ -474,7 +458,7 @@ class PmsConnect(object):
return request
def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''):
def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''):
"""
Return processed and validated list of recently added items.
@@ -889,7 +873,7 @@ class PmsConnect(object):
'collections': show_details.get('collections', []),
'guids': show_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')),
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
}
@@ -962,7 +946,7 @@ class PmsConnect(object):
'collections': show_details.get('collections', []),
'guids': show_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title')),
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
}
@@ -1063,7 +1047,7 @@ class PmsConnect(object):
'collections': collections,
'guids': guids,
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')),
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
}
@@ -1117,7 +1101,7 @@ class PmsConnect(object):
'collections': album_details.get('collections', []),
'guids': album_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'),
track_artist),
track_artist),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
}
@@ -1217,8 +1201,9 @@ class PmsConnect(object):
'labels': photo_album_details.get('labels', []),
'collections': photo_album_details.get('collections', []),
'guids': photo_album_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name,
helpers.get_xml_attr(metadata_main, 'title')),
'full_title': '{} - {}'.format(
helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name,
helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
}
@@ -1385,7 +1370,8 @@ class PmsConnect(object):
'video_codec_level': helpers.get_xml_attr(stream, 'level'),
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'),
'video_chroma_subsampling': helpers.get_xml_attr(stream, 'chromaSubsampling'),
'video_chroma_subsampling': helpers.get_xml_attr(stream,
'chromaSubsampling'),
'video_color_primaries': helpers.get_xml_attr(stream, 'colorPrimaries'),
'video_color_range': helpers.get_xml_attr(stream, 'colorRange'),
'video_color_space': helpers.get_xml_attr(stream, 'colorSpace'),
@@ -1423,7 +1409,8 @@ class PmsConnect(object):
'subtitle_container': helpers.get_xml_attr(stream, 'container'),
'subtitle_format': helpers.get_xml_attr(stream, 'format'),
'subtitle_forced': int(helpers.get_xml_attr(stream, 'forced') == '1'),
'subtitle_location': 'external' if helpers.get_xml_attr(stream, 'key') else 'embedded',
'subtitle_location': 'external' if helpers.get_xml_attr(stream,
'key') else 'embedded',
'subtitle_language': helpers.get_xml_attr(stream, 'language'),
'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode'),
'selected': int(helpers.get_xml_attr(stream, 'selected') == '1')
@@ -1673,7 +1660,8 @@ class PmsConnect(object):
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default')
player_details = {'ip_address': helpers.get_xml_attr(player_info, 'address').split('::ffff:')[-1],
'ip_address_public': helpers.get_xml_attr(player_info, 'remotePublicAddress').split('::ffff:')[-1],
'ip_address_public':
helpers.get_xml_attr(player_info, 'remotePublicAddress').split('::ffff:')[-1],
'device': helpers.get_xml_attr(player_info, 'device'),
'platform': platform,
'platform_name': platform_name,
@@ -1681,7 +1669,8 @@ class PmsConnect(object):
'product': helpers.get_xml_attr(player_info, 'product'),
'product_version': helpers.get_xml_attr(player_info, 'version'),
'profile': helpers.get_xml_attr(player_info, 'profile'),
'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info, 'product'),
'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info,
'product'),
'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier'),
'state': helpers.get_xml_attr(player_info, 'state'),
'local': int(helpers.get_xml_attr(player_info, 'local') == '1'),
@@ -1731,20 +1720,27 @@ class PmsConnect(object):
'transcode_audio_channels': helpers.get_xml_attr(transcode_info, 'audioChannels'),
'transcode_audio_codec': helpers.get_xml_attr(transcode_info, 'audioCodec'),
'transcode_video_codec': helpers.get_xml_attr(transcode_info, 'videoCodec'),
'transcode_width': helpers.get_xml_attr(transcode_info, 'width'), # Blank but keep for backwards compatibility
'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), # Blank but keep backwards compatibility
'transcode_width': helpers.get_xml_attr(transcode_info, 'width'),
# Blank but keep for backwards compatibility
'transcode_height': helpers.get_xml_attr(transcode_info, 'height'),
# Blank but keep backwards compatibility
'transcode_container': helpers.get_xml_attr(transcode_info, 'container'),
'transcode_protocol': helpers.get_xml_attr(transcode_info, 'protocol'),
'transcode_hw_requested': int(helpers.get_xml_attr(transcode_info, 'transcodeHwRequested') == '1'),
'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_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': int(helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1'),
'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info,
'transcodeHwEncodingTitle'),
'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'),
'throttled': '1' if helpers.get_xml_attr(transcode_info, 'throttled') == '1' else '0' # Keep for backwards compatibility
'throttled': '1' if helpers.get_xml_attr(transcode_info, 'throttled') == '1' else '0'
# Keep for backwards compatibility
}
else:
transcode_session = False
@@ -1773,8 +1769,10 @@ class PmsConnect(object):
}
# 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)
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)
# Determine if a synced version is being played
sync_id = synced_session_data = synced_item_details = None
@@ -1788,7 +1786,8 @@ class PmsConnect(object):
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
rating_key_filter=[rating_key, parent_rating_key, grandparent_rating_key])
rating_key_filter=[rating_key, parent_rating_key,
grandparent_rating_key])
if synced_items:
synced_item_details = synced_items[0]
sync_id = synced_item_details['sync_id']
@@ -1810,9 +1809,11 @@ class PmsConnect(object):
media_info_all = synced_session_data.getElementsByTagName('Media')
else:
media_info_all = session.getElementsByTagName('Media')
stream_media_info = next((m for m in media_info_all if helpers.get_xml_attr(m, 'selected') == '1'), media_info_all[0])
stream_media_info = next((m for m in media_info_all if helpers.get_xml_attr(m, 'selected') == '1'),
media_info_all[0])
part_info_all = stream_media_info.getElementsByTagName('Part')
stream_media_parts_info = next((p for p in part_info_all if helpers.get_xml_attr(p, 'selected') == '1'), part_info_all[0])
stream_media_parts_info = next((p for p in part_info_all if helpers.get_xml_attr(p, 'selected') == '1'),
part_info_all[0])
# Get the stream details
video_stream_info = audio_stream_info = subtitle_stream_info = None
@@ -1834,7 +1835,8 @@ class PmsConnect(object):
video_id = helpers.get_xml_attr(video_stream_info, 'id')
video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'),
'stream_video_bit_depth': helpers.get_xml_attr(video_stream_info, 'bitDepth'),
'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info, 'chromaSubsampling'),
'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info,
'chromaSubsampling'),
'stream_video_color_primaries': helpers.get_xml_attr(video_stream_info, 'colorPrimaries'),
'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'),
'stream_video_color_space': helpers.get_xml_attr(video_stream_info, 'colorSpace'),
@@ -1844,7 +1846,8 @@ class PmsConnect(object):
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
'stream_video_language_code': helpers.get_xml_attr(video_stream_info, 'languageCode'),
'stream_video_scan_type': helpers.get_xml_attr(video_stream_info, 'scanType'),
'stream_video_decision': helpers.get_xml_attr(video_stream_info, 'decision') or 'direct play'
'stream_video_decision': helpers.get_xml_attr(video_stream_info,
'decision') or 'direct play'
}
else:
video_details = {'stream_video_bitrate': '',
@@ -1867,10 +1870,12 @@ class PmsConnect(object):
audio_details = {'stream_audio_bitrate': helpers.get_xml_attr(audio_stream_info, 'bitrate'),
'stream_audio_bitrate_mode': helpers.get_xml_attr(audio_stream_info, 'bitrateMode'),
'stream_audio_sample_rate': helpers.get_xml_attr(audio_stream_info, 'samplingRate'),
'stream_audio_channel_layout_': helpers.get_xml_attr(audio_stream_info, 'audioChannelLayout'),
'stream_audio_channel_layout_': helpers.get_xml_attr(audio_stream_info,
'audioChannelLayout'),
'stream_audio_language': helpers.get_xml_attr(audio_stream_info, 'language'),
'stream_audio_language_code': helpers.get_xml_attr(audio_stream_info, 'languageCode'),
'stream_audio_decision': helpers.get_xml_attr(audio_stream_info, 'decision') or 'direct play'
'stream_audio_decision': helpers.get_xml_attr(audio_stream_info,
'decision') or 'direct play'
}
else:
audio_details = {'stream_audio_bitrate': '',
@@ -1888,12 +1893,15 @@ 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': int(helpers.get_xml_attr(subtitle_stream_info, 'forced') == '1'),
'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'),
'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info,
'languageCode'),
'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision'),
'stream_subtitle_transient': int(helpers.get_xml_attr(subtitle_stream_info, 'transient') == '1')
'stream_subtitle_transient': int(
helpers.get_xml_attr(subtitle_stream_info, 'transient') == '1')
}
else:
subtitle_selected = None
@@ -1913,7 +1921,8 @@ class PmsConnect(object):
view_offset = helpers.get_xml_attr(session, 'viewOffset')
if indexes == 'sd':
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id')
bif_thumb = '/library/parts/{part_id}/indexes/sd/{view_offset}'.format(part_id=part_id, view_offset=view_offset)
bif_thumb = '/library/parts/{part_id}/indexes/sd/{view_offset}'.format(part_id=part_id,
view_offset=view_offset)
else:
bif_thumb = ''
@@ -1930,14 +1939,19 @@ class PmsConnect(object):
'stream_aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'),
'stream_audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
'stream_audio_channels': stream_audio_channels,
'stream_audio_channel_layout': audio_details.get('stream_audio_channel_layout_') or common.AUDIO_CHANNELS.get(stream_audio_channels, stream_audio_channels),
'stream_audio_channel_layout': audio_details.get(
'stream_audio_channel_layout_') or common.AUDIO_CHANNELS.get(stream_audio_channels,
stream_audio_channels),
'stream_video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'stream_video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'),
'stream_video_resolution': stream_video_resolution,
'stream_video_height': helpers.get_xml_attr(stream_media_info, 'height'),
'stream_video_width': helpers.get_xml_attr(stream_media_info, 'width'),
'stream_duration': helpers.get_xml_attr(stream_media_info, 'duration') or helpers.get_xml_attr(session, 'duration'),
'stream_container_decision': 'direct play' if sync_id else helpers.get_xml_attr(stream_media_parts_info, 'decision').replace('directplay', 'direct play'),
'stream_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'),
'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,
@@ -2004,7 +2018,8 @@ class PmsConnect(object):
'aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'),
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(),
'video_full_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(),
'video_full_resolution': helpers.get_xml_attr(stream_media_info,
'videoResolution').lower(),
'video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'),
'video_profile': helpers.get_xml_attr(stream_media_info, 'videoProfile'),
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
@@ -2031,9 +2046,11 @@ class PmsConnect(object):
# Get the media info, fallback to first item if match id is not found
source_medias = metadata_details.pop('media_info', [])
source_media_details = next((m for m in source_medias if m['id'] == media_id), next((m for m in source_medias), {}))
source_media_details = next((m for m in source_medias if m['id'] == media_id),
next((m for m in source_medias), {}))
source_media_parts = source_media_details.pop('parts', [])
source_media_part_details = next((p for p in source_media_parts if p['id'] == part_id), next((p for p in source_media_parts), {}))
source_media_part_details = next((p for p in source_media_parts if p['id'] == part_id),
next((p for p in source_media_parts), {}))
source_media_part_streams = source_media_part_details.pop('streams', [])
source_video_details = {'id': '',
@@ -2080,13 +2097,16 @@ class PmsConnect(object):
}
if video_id:
source_video_details = next((p for p in source_media_part_streams if p['id'] == video_id),
next((p for p in source_media_part_streams if p['type'] == '1'), source_video_details))
next((p for p in source_media_part_streams if p['type'] == '1'),
source_video_details))
if audio_id:
source_audio_details = next((p for p in source_media_part_streams if p['id'] == audio_id),
next((p for p in source_media_part_streams if p['type'] == '2'), source_audio_details))
next((p for p in source_media_part_streams if p['type'] == '2'),
source_audio_details))
if subtitle_id:
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id),
next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details))
next((p for p in source_media_part_streams if p['type'] == '3'),
source_subtitle_details))
# Override the thumb for clips
if media_type == 'clip' and metadata_details.get('extra_type') and metadata_details['art']:
@@ -2107,7 +2127,8 @@ class PmsConnect(object):
transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels'])
# Generate a combined transcode decision value
if video_details['stream_video_decision'] == 'transcode' or audio_details['stream_audio_decision'] == 'transcode':
if video_details['stream_video_decision'] == 'transcode' or audio_details[
'stream_audio_decision'] == 'transcode':
transcode_decision = 'transcode'
elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy':
transcode_decision = 'copy'
@@ -2198,7 +2219,8 @@ class PmsConnect(object):
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
try:
quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
quailtiy_bitrate = min(
b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate)
quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
except ValueError:
quality_profile = 'Original'
@@ -2463,7 +2485,7 @@ class PmsConnect(object):
'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration')
}
}
children_results_list[media_type].append(children_output)
output = {'results_count': sum(len(v) for k, v in children_results_list.items()),
@@ -2630,11 +2652,13 @@ class PmsConnect(object):
sort_type = ''
if str(section_id).isdigit():
library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key, output_format='xml')
library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key,
output_format='xml')
elif str(rating_key).isdigit():
library_data = self.get_metadata_children(str(rating_key), output_format='xml')
else:
logger.warn("Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
logger.warn(
"Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.")
return []
try:
@@ -2735,7 +2759,8 @@ class PmsConnect(object):
for library in libraries_list:
section_type = library['section_type']
section_id = library['section_id']
children_list = self.get_library_children_details(section_id=section_id, section_type=section_type, count='1')
children_list = self.get_library_children_details(section_id=section_id, section_type=section_type,
count='1')
if children_list:
library_stats = {'section_id': section_id,
@@ -2749,34 +2774,40 @@ class PmsConnect(object):
}
if section_type == 'show':
parent_list = self.get_library_children_details(section_id=section_id, section_type='season', count='1')
parent_list = self.get_library_children_details(section_id=section_id, section_type='season',
count='1')
if parent_list:
parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats)
child_list = self.get_library_children_details(section_id=section_id, section_type='episode', count='1')
child_list = self.get_library_children_details(section_id=section_id, section_type='episode',
count='1')
if child_list:
child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats)
if section_type == 'artist':
parent_list = self.get_library_children_details(section_id=section_id, section_type='album', count='1')
parent_list = self.get_library_children_details(section_id=section_id, section_type='album',
count='1')
if parent_list:
parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats)
child_list = self.get_library_children_details(section_id=section_id, section_type='track', count='1')
child_list = self.get_library_children_details(section_id=section_id, section_type='track',
count='1')
if child_list:
child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats)
if section_type == 'photo':
parent_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
parent_list = self.get_library_children_details(section_id=section_id, section_type='picture',
count='1')
if parent_list:
parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats)
child_list = self.get_library_children_details(section_id=section_id, section_type='clip', count='1')
child_list = self.get_library_children_details(section_id=section_id, section_type='clip',
count='1')
if child_list:
child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats)
@@ -2896,7 +2927,7 @@ class PmsConnect(object):
for h in hubs:
if helpers.get_xml_attr(h, 'size') == '0' or \
helpers.get_xml_attr(h, 'type') not in search_results_list:
helpers.get_xml_attr(h, 'type') not in search_results_list:
continue
if h.getElementsByTagName('Video'):
@@ -2971,7 +3002,8 @@ class PmsConnect(object):
section_id = metadata['section_id']
library_name = metadata['library_name']
except Exception as e:
logger.warn("Tautulli Pmsconnect :: Unable to get grandparent_rating_key for get_rating_keys_list: %s." % e)
logger.warn(
"Tautulli Pmsconnect :: Unable to get grandparent_rating_key for get_rating_keys_list: %s." % e)
return {}
# get parent_rating_keys
@@ -3035,8 +3067,8 @@ class PmsConnect(object):
key = int(parent_index) if match_type == 'index' else parent_title
parents.update({key:
{'rating_key': int(parent_rating_key),
'children': children}
{'rating_key': int(parent_rating_key),
'children': children}
})
key = 0 if match_type == 'index' else title
@@ -3097,7 +3129,8 @@ class PmsConnect(object):
# Catch the malformed XML on certain PMX version.
# XML parser helper returns empty list if there is an error parsing XML
if updater_status == []:
logger.warn("Plex API updater XML is broken on the current PMS version. Please update your PMS manually.")
logger.warn(
"Plex API updater XML is broken on the current PMS version. Please update your PMS manually.")
logger.info("Tautulli is unable to check for Plex updates. Disabling check for Plex updates.")
# Disable check for Plex updates

View File

@@ -15,24 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from bs4 import BeautifulSoup
import collections
from xml.dom import minidom
import collections
import requests
from bs4 import BeautifulSoup
from requests.packages import urllib3
import jellypy
if jellypy.PYTHON2:
import lock
import logger
else:
from jellypy import lock
from jellypy import logger
from jellypy import lock
from jellypy import logger
# Dictionary with last request times, for rate limiting.
last_requests = collections.defaultdict(int)
@@ -182,7 +174,7 @@ def request_response2(url, method="get", auto_raise=True,
err_msg = "Unable to connect to remote host because of a SSL error."
else:
err_msg = "Unable to connect to remote host because of a SSL error, " \
"with certificate verification turned off: {}".format(e)
"with certificate verification turned off: {}".format(e)
except requests.ConnectionError:
err_msg = "Unable to connect to remote host. Check if the remote host is up and running."

View File

@@ -14,19 +14,11 @@
#
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
import cherrypy
import jellypy
if jellypy.PYTHON2:
import common
import users
else:
from jellypy import common
from jellypy import users
from jellypy import common
from jellypy import users
def get_session_info():
@@ -43,6 +35,7 @@ def get_session_info():
return _session
def get_session_user():
"""
Returns the user_id for the current logged in session
@@ -50,6 +43,7 @@ def get_session_user():
_session = get_session_info()
return _session['user'] if _session['user_group'] == 'guest' and _session['user'] else None
def get_session_user_id():
"""
Returns the user_id for the current logged in session
@@ -80,6 +74,7 @@ def get_session_shared_libraries():
user_details = users.Users().get_details(user_id=get_session_user_id())
return tuple(str(s) for s in user_details['shared_libraries'])
def get_session_library_filters():
"""
Returns a dict of library filters for the current logged in session
@@ -91,6 +86,7 @@ def get_session_library_filters():
filters = users.Users().get_filters(user_id=get_session_user_id())
return filters
def get_session_library_filters_type(filters, media_type=None):
"""
Returns a dict of library filters for the current logged in session
@@ -115,6 +111,7 @@ def get_session_library_filters_type(filters, media_type=None):
return content_rating, tuple(f.lower() for f in labels)
def allow_session_user(user_id):
"""
Returns True or False if the user_id is allowed for the current logged in session
@@ -124,6 +121,7 @@ def allow_session_user(user_id):
return False
return True
def allow_session_library(section_id):
"""
Returns True or False if the section_id is allowed for the current logged in session
@@ -133,13 +131,14 @@ def allow_session_library(section_id):
return False
return True
def friendly_name_to_username(list_of_dicts):
"""
Reverts the friendly name back to the username of the current logged in session
"""
session_user = get_session_user()
session_user_id = get_session_user_id()
if session_user_id:
for d in list_of_dicts:
if 'friendly_name' in d and d['friendly_name'] != session_user:
@@ -147,12 +146,13 @@ def friendly_name_to_username(list_of_dicts):
return list_of_dicts
def filter_session_info(list_of_dicts, filter_key=None):
"""
Filters a list of dictionary items to only return the info for the current logged in session
"""
session_user_id = get_session_user_id()
if not session_user_id:
return list_of_dicts
@@ -162,13 +162,13 @@ def filter_session_info(list_of_dicts, filter_key=None):
list_of_dicts = friendly_name_to_username(list_of_dicts)
if filter_key == 'user_id' and session_user_id:
return [d for d in list_of_dicts if str(d.get('user_id','')) == session_user_id]
return [d for d in list_of_dicts if str(d.get('user_id', '')) == session_user_id]
elif filter_key == 'section_id' and session_library_ids:
new_list_of_dicts = []
for d in list_of_dicts:
if str(d.get('section_id','')) not in session_library_ids:
if str(d.get('section_id', '')) not in session_library_ids:
continue
if d.get('media_type'):
@@ -198,6 +198,7 @@ def filter_session_info(list_of_dicts, filter_key=None):
return list_of_dicts
def mask_session_info(list_of_dicts, mask_metadata=True):
"""
Masks user info in a list of dictionary items to only display info for the current logged in session
@@ -249,7 +250,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
if not mask_metadata:
continue
if str(d.get('section_id','')) not in session_library_ids:
if str(d.get('section_id', '')) not in session_library_ids:
for k, v in metadata_to_mask.items():
if k in d: d[k] = metadata_to_mask[k]
continue
@@ -257,7 +258,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
media_type = d.get('media_type')
if media_type:
f_content_rating, f_labels = get_session_library_filters_type(session_library_filters,
media_type=d['media_type'])
media_type=d['media_type'])
d_content_rating = d.get('content_rating', '')
d_labels = tuple(f.lower() for f in d.get('labels', ()))
@@ -277,4 +278,4 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
for k, v in metadata_to_mask.items():
if k in d: d[k] = metadata_to_mask[k]
return list_of_dicts
return list_of_dicts

View File

@@ -14,34 +14,19 @@
#
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
from future.builtins import object
from future.moves.urllib.parse import parse_qsl
from urllib.parse import parse_qsl
import httpagentparser
import jellypy
if jellypy.PYTHON2:
import common
import database
import datatables
import helpers
import libraries
import logger
import plextv
import session
else:
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import plextv
from jellypy import session
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import plextv
from jellypy import session
def refresh_users():
@@ -847,7 +832,8 @@ class Users(object):
return filters_list
def set_user_login(self, user_id=None, user=None, user_group=None, ip_address=None, host=None, user_agent=None, success=0):
def set_user_login(self, user_id=None, user=None, user_group=None, ip_address=None, host=None, user_agent=None,
success=0):
if user_id is None or str(user_id).isdigit():
monitor_db = database.MonitorDatabase()
@@ -947,4 +933,4 @@ class Users(object):
return True
except Exception as e:
logger.warn("Tautulli Users :: Unable to execute database query for delete_login_log: %s." % e)
return False
return False

View File

@@ -15,7 +15,5 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.6.5"

View File

@@ -15,11 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
import json
import os
import platform
@@ -28,20 +23,13 @@ import subprocess
import tarfile
import jellypy
if jellypy.PYTHON2:
import common
import helpers
import logger
import request
else:
from jellypy import common
from jellypy import helpers
from jellypy import logger
from jellypy import request
from jellypy import common
from jellypy import helpers
from jellypy import logger
from jellypy import request
def runGit(args):
if jellypy.CONFIG.GIT_PATH:
git_locations = ['"' + jellypy.CONFIG.GIT_PATH + '"']
else:
@@ -57,7 +45,8 @@ def runGit(args):
try:
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + jellypy.PROG_DIR)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=jellypy.PROG_DIR)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True,
cwd=jellypy.PROG_DIR)
output, err = p.communicate()
output = output.strip().decode()
@@ -79,7 +68,6 @@ def runGit(args):
def get_version():
if jellypy.FROZEN and common.PLATFORM == 'Windows':
jellypy.INSTALL_TYPE = 'windows'
current_version, current_branch = get_version_from_file()
@@ -270,9 +258,9 @@ def check_github(scheduler=False, notify=False, use_cache=False):
if notify:
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate',
'plexpy_download_info': release,
'plexpy_update_commit': jellypy.LATEST_VERSION,
'plexpy_update_behind': jellypy.COMMITS_BEHIND})
'plexpy_download_info': release,
'plexpy_update_commit': jellypy.LATEST_VERSION,
'plexpy_update_behind': jellypy.COMMITS_BEHIND})
if jellypy.PYTHON2:
logger.warn('Tautulli is running using Python 2. Unable to run automatic update.')

View File

@@ -17,9 +17,6 @@
# Mostly borrowed from https://github.com/trakt/Plex-Trakt-Scrobbler
from __future__ import unicode_literals
from future.builtins import str
import json
import ssl
import threading
@@ -29,19 +26,11 @@ import certifi
import websocket
import jellypy
if jellypy.PYTHON2:
import activity_handler
import activity_pinger
import activity_processor
import database
import logger
else:
from jellypy import activity_handler
from jellypy import activity_pinger
from jellypy import activity_processor
from jellypy import database
from jellypy import logger
from jellypy import activity_handler
from jellypy import activity_pinger
from jellypy import activity_processor
from jellypy import database
from jellypy import logger
name = 'websocket'
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)

View File

@@ -20,28 +20,20 @@
# Form based authentication for CherryPy. Requires the
# Session tool to be loaded.
from future.builtins import object
from datetime import datetime, timedelta
from future.moves.urllib.parse import quote, unquote
from urllib.parse import quote, unquote
import cherrypy
from hashing_passwords import check_hash
import jwt
from hashing_passwords import check_hash
import jellypy
if jellypy.PYTHON2:
import logger
from database import MonitorDatabase
from helpers import timestamp
from users import Users, refresh_users
from plextv import PlexTV
else:
from jellypy import logger
from jellypy.database import MonitorDatabase
from jellypy.helpers import timestamp
from jellypy.users import Users, refresh_users
from jellypy.plextv import PlexTV
from jellypy import logger
from jellypy.database import MonitorDatabase
from jellypy.helpers import timestamp
from jellypy.users import Users, refresh_users
from jellypy.plextv import PlexTV
# Monkey patch SameSite support into cookies.
# https://stackoverflow.com/a/50813092
@@ -199,6 +191,7 @@ def check_auth(*args, **kwargs):
def requireAuth(*conditions):
"""A decorator that appends conditions to the auth.require config
variable."""
def decorate(f):
if not hasattr(f, '_cp_config'):
f._cp_config = dict()
@@ -206,6 +199,7 @@ def requireAuth(*conditions):
f._cp_config['auth.require'] = []
f._cp_config['auth.require'].extend(conditions)
return f
return decorate
@@ -228,11 +222,13 @@ def name_is(user_name):
def any_of(*conditions):
"""Returns True if any of the conditions match"""
def check():
for c in conditions:
if c():
return True
return False
return check
@@ -240,11 +236,13 @@ def any_of(*conditions):
# needed if you want to use it inside of an any_of(...) condition
def all_of(*conditions):
"""Returns True if all of the conditions match"""
def check():
for c in conditions:
if not c():
return False
return True
return check

View File

@@ -15,14 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import object
from future.builtins import str
from backports import csv
from io import open, BytesIO
import base64
import csv
import json
import linecache
import os
@@ -30,88 +24,52 @@ import shutil
import sys
import threading
import zipfile
from future.moves.urllib.parse import urlencode
from io import open, BytesIO
import cherrypy
import mako.exceptions
import mako.template
import websocket
from cherrypy import NotFound
from cherrypy.lib.static import serve_file, serve_fileobj, serve_download
from cherrypy._cperror import NotFound
from hashing_passwords import make_hash
from mako.lookup import TemplateLookup
import mako.template
import mako.exceptions
import websocket
import jellypy
if jellypy.PYTHON2:
import activity_pinger
import common
import config
import database
import datafactory
import exporter
import graphs
import helpers
import http_handler
import libraries
import log_reader
import logger
import newsletter_handler
import newsletters
import mobile_app
import notification_handler
import notifiers
import plextv
import plexivity_import
import plexwatch_import
import pmsconnect
import users
import versioncheck
import web_socket
import webstart
from api2 import API2
from helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
from session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
from webauth import AuthController, requireAuth, member_of, check_auth
if common.PLATFORM == 'Windows':
import windows
elif common.PLATFORM == 'Darwin':
import macos
else:
from jellypy import activity_pinger
from jellypy import common
from jellypy import config
from jellypy import database
from jellypy import datafactory
from jellypy import exporter
from jellypy import graphs
from jellypy import helpers
from jellypy import http_handler
from jellypy import libraries
from jellypy import log_reader
from jellypy import logger
from jellypy import newsletter_handler
from jellypy import newsletters
from jellypy import mobile_app
from jellypy import notification_handler
from jellypy import notifiers
from jellypy import plextv
from jellypy import plexivity_import
from jellypy import plexwatch_import
from jellypy import pmsconnect
from jellypy import users
from jellypy import versioncheck
from jellypy import web_socket
from jellypy import webstart
from jellypy.api2 import API2
from jellypy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
from jellypy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
from jellypy.webauth import AuthController, requireAuth, member_of, check_auth
if common.PLATFORM == 'Windows':
from jellypy import windows
elif common.PLATFORM == 'Darwin':
from jellypy import macos
from jellypy import activity_pinger
from jellypy import common
from jellypy import config
from jellypy import database
from jellypy import datafactory
from jellypy import exporter
from jellypy import graphs
from jellypy import helpers
from jellypy import http_handler
from jellypy import libraries
from jellypy import log_reader
from jellypy import logger
from jellypy import newsletter_handler
from jellypy import newsletters
from jellypy import mobile_app
from jellypy import notification_handler
from jellypy import notifiers
from jellypy import plextv
from jellypy import plexivity_import
from jellypy import plexwatch_import
from jellypy import pmsconnect
from jellypy import users
from jellypy import versioncheck
from jellypy import web_socket
from jellypy import webstart
from jellypy.api2 import API2
from jellypy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
from jellypy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
from jellypy.webauth import AuthController, requireAuth, member_of, check_auth
if common.PLATFORM == 'Windows':
from jellypy import windows
elif common.PLATFORM == 'Darwin':
from jellypy import macos
def serve_template(templatename, **kwargs):
@@ -181,7 +139,6 @@ class BaseRedirect(object):
class WebInterface(object):
auth = AuthController()
def __init__(self):
@@ -195,7 +152,6 @@ class WebInterface(object):
else:
raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT + "welcome")
##### Welcome #####
@cherrypy.expose
@@ -265,7 +221,6 @@ class WebInterface(object):
if servers_list:
return servers_list
##### Home #####
@cherrypy.expose
@@ -455,7 +410,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Flush recently added failed.'}
##### Libraries #####
@cherrypy.expose
@@ -741,7 +695,8 @@ class WebInterface(object):
result = None
if result:
return serve_template(templatename="library_recently_added.html", data=result['recently_added'], title="Recently Added")
return serve_template(templatename="library_recently_added.html", data=result['recently_added'],
title="Recently Added")
else:
logger.warn("Unable to retrieve data for library_recently_added.")
return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added")
@@ -1170,7 +1125,7 @@ class WebInterface(object):
result = library_data.undelete(section_id=section_id, section_name=section_name)
if result:
if section_id:
msg ='section_id %s' % section_id
msg = 'section_id %s' % section_id
elif section_name:
msg = 'section_name %s' % section_name
return {'result': 'success', 'message': 'Re-added library with %s.' % msg}
@@ -1356,7 +1311,8 @@ class WebInterface(object):
result = None
status_message = 'An error occured.'
return serve_template(templatename="edit_user.html", title="Edit User", data=result, status_message=status_message)
return serve_template(templatename="edit_user.html", title="Edit User", data=result,
status_message=status_message)
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -1801,13 +1757,12 @@ class WebInterface(object):
result = user_data.undelete(user_id=user_id, username=username)
if result:
if user_id:
msg ='user_id %s' % user_id
msg = 'user_id %s' % user_id
elif username:
msg = 'username %s' % username
return {'result': 'success', 'message': 'Re-added user with %s.' % msg}
return {'result': 'error', 'message': 'Unable to re-add user. Invalid user_id or username.'}
##### History #####
@cherrypy.expose
@@ -2093,7 +2048,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'No row ids received.'}
##### Graphs #####
@cherrypy.expose
@@ -2570,7 +2524,8 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
@addtoapi()
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs):
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None,
**kwargs):
""" Get graph data by stream type by top 10 platforms.
```
@@ -2617,7 +2572,6 @@ class WebInterface(object):
return serve_template(templatename="history_table_modal.html", title="History Data", data=kwargs)
##### Sync #####
@cherrypy.expose
@@ -2673,7 +2627,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Missing client ID and sync ID.'}
##### Logs #####
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -3026,9 +2979,9 @@ class WebInterface(object):
def log_js_errors(self, page, message, file, line, **kwargs):
""" Logs javascript errors from the web interface. """
logger.error("WebUI :: /%s : %s. (%s:%s)" % (page.rpartition('/')[-1],
message,
file.rpartition('/')[-1].partition('?')[0],
line))
message,
file.rpartition('/')[-1].partition('?')[0],
line))
return "js error logged."
@cherrypy.expose
@@ -3047,7 +3000,6 @@ class WebInterface(object):
except IOError as e:
return "Log file not found."
##### Settings #####
@cherrypy.expose
@@ -3525,9 +3477,9 @@ class WebInterface(object):
result = notifiers.get_notifier_config(notifier_id=notifier_id, mask_passwords=True)
parameters = [
{'name': param['name'], 'type': param['type'], 'value': param['value']}
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']
]
{'name': param['name'], 'type': param['type'], 'value': param['value']}
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']
]
return serve_template(templatename="notifier_config.html", notifier=result, parameters=parameters)
@@ -3646,7 +3598,8 @@ class WebInterface(object):
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='', **kwargs):
def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='',
**kwargs):
""" Send a notification using Tautulli.
```
@@ -3713,7 +3666,8 @@ class WebInterface(object):
redirect_uri=redirect_uri)
if url:
return {'result': 'success', 'msg': 'Confirm Authorization. Check pop-up blocker if no response.', 'url': url}
return {'result': 'success', 'msg': 'Confirm Authorization. Check pop-up blocker if no response.',
'url': url}
else:
return {'result': 'error', 'msg': 'Failed to retrieve authorization url.'}
@@ -3727,8 +3681,8 @@ class WebInterface(object):
if access_token:
return "Facebook authorization successful. Tautulli can send notification to Facebook. " \
"Your Facebook access token is:" \
"<pre>{0}</pre>You may close this page.".format(access_token)
"Your Facebook access token is:" \
"<pre>{0}</pre>You may close this page.".format(access_token)
else:
return "Failed to request authorization from Facebook. Check the Tautulli logs for details.<br />You may close this page."
@@ -3807,7 +3761,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Device not registered.'}
@cherrypy.expose
@requireAuth(member_of("admin"))
def get_mobile_device_config_modal(self, mobile_device_id=None, **kwargs):
@@ -4240,7 +4193,8 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def generate_api_key(self, device=None, **kwargs):
apikey = ''
while not apikey or apikey == jellypy.CONFIG.API_KEY or mobile_app.get_mobile_device_by_token(device_token=apikey):
while not apikey or apikey == jellypy.CONFIG.API_KEY or mobile_app.get_mobile_device_by_token(
device_token=apikey):
apikey = jellypy.generate_uuid()
logger.info("New API key generated.")
@@ -4739,7 +4693,6 @@ class WebInterface(object):
else:
return "Plex log folder not set in the settings."
if log_file and os.path.isfile(log_file_path):
return serve_download(log_file_path, name=log_file)
else:
@@ -4847,7 +4800,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Failed to delete lookup info.'}
##### Search #####
@cherrypy.expose
@@ -4915,7 +4867,6 @@ class WebInterface(object):
logger.warn("Unable to retrieve data for get_search_results_children.")
return serve_template(templatename="info_search_results_list.html", data=None, title="Search Result List")
##### Update Metadata #####
@cherrypy.expose
@@ -5304,7 +5255,8 @@ class WebInterface(object):
media_type = kwargs['type']
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_recently_added_details(start=start, count=count, media_type=media_type, section_id=section_id)
result = pms_connect.get_recently_added_details(start=start, count=count, media_type=media_type,
section_id=section_id)
if result:
return result
@@ -6411,7 +6363,7 @@ class WebInterface(object):
subject=subject,
body=body,
message=message,
**kwargs)
**kwargs)
return {'result': 'success', 'message': 'Newsletter queued.'}
else:
logger.debug("Unable to send %snewsletter, invalid newsletter_id %s." % (test, newsletter_id))
@@ -6897,7 +6849,6 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Failed to delete export.'}
@cherrypy.expose
@requireAuth(member_of("admin"))
def exporter_docs(self, **kwargs):

View File

@@ -21,16 +21,10 @@ import sys
import cherrypy
import jellypy
if jellypy.PYTHON2:
import logger
import webauth
from helpers import create_https_certificates
from webserve import WebInterface, BaseRedirect
else:
from jellypy import logger
from jellypy import webauth
from jellypy.helpers import create_https_certificates
from jellypy.webserve import WebInterface, BaseRedirect
from jellypy import logger
from jellypy import webauth
from jellypy.helpers import create_https_certificates
from jellypy.webserve import WebInterface, BaseRedirect
def start():
@@ -64,7 +58,6 @@ def restart():
def initialize(options):
# HTTPS stuff stolen from sickbeard
enable_https = options['enable_https']
https_cert = options['https_cert']
@@ -225,7 +218,7 @@ def initialize(options):
'tools.sessions.on': False,
'tools.auth.on': False
},
#'/pms_image_proxy': {
# '/pms_image_proxy': {
# 'tools.staticdir.on': True,
# 'tools.staticdir.dir': os.path.join(jellypy.CONFIG.CACHE_DIR, 'images'),
# 'tools.caching.on': True,
@@ -235,10 +228,11 @@ def initialize(options):
# 'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
# 'tools.auth.on': False,
# 'tools.sessions.on': False
#},
# },
'/favicon.ico': {
'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.abspath(os.path.join(jellypy.PROG_DIR, 'data/interfaces/default/images/favicon/favicon.ico')),
'tools.staticfile.filename': os.path.abspath(
os.path.join(jellypy.PROG_DIR, 'data/interfaces/default/images/favicon/favicon.ico')),
'tools.caching.on': True,
'tools.caching.force': True,
'tools.caching.delay': 0,
@@ -256,7 +250,7 @@ def initialize(options):
try:
logger.info("Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol,
options['http_host'], options['http_port'], options['http_root'])
#cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
# cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
if not jellypy.DEV:
cherrypy.server.start()
else: