More rename, more -python2
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
@@ -13,26 +13,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 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
|
||||
@@ -40,7 +29,6 @@ else:
|
||||
from jellypy import notification_handler
|
||||
from jellypy import pmsconnect
|
||||
|
||||
|
||||
ACTIVITY_SCHED = None
|
||||
|
||||
RECENTLY_ADDED_QUEUE = {}
|
||||
@@ -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()
|
||||
@@ -460,7 +449,8 @@ 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."
|
||||
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
|
||||
@@ -479,7 +469,8 @@ 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."
|
||||
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
|
||||
@@ -618,7 +609,8 @@ 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"
|
||||
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,7 +619,8 @@ 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. " \
|
||||
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)
|
||||
@@ -637,10 +630,12 @@ 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. " \
|
||||
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"
|
||||
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)
|
||||
|
@@ -13,36 +13,20 @@
|
||||
# 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
|
||||
|
||||
|
||||
monitor_lock = threading.Lock()
|
||||
ext_ping_count = 0
|
||||
ext_ping_error = None
|
||||
@@ -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,13 +117,15 @@ 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."
|
||||
logger.info(
|
||||
"Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
|
||||
% stream['user'])
|
||||
# Set the buffer trigger time
|
||||
monitor_db.action('UPDATE sessions '
|
||||
@@ -144,9 +133,11 @@ 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'})
|
||||
|
||||
logger.debug("Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||
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']))
|
||||
@@ -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) \
|
||||
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'})
|
||||
jellypy.NOTIFY_QUEUE.put(
|
||||
{'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||
|
||||
else:
|
||||
# The user has stopped playing a stream
|
||||
@@ -173,13 +168,17 @@ 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) \
|
||||
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'})
|
||||
|
||||
@@ -197,12 +196,14 @@ 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. " \
|
||||
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. " \
|
||||
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"
|
||||
@@ -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...")
|
||||
|
||||
|
@@ -13,22 +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 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
|
||||
@@ -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 "
|
||||
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']),
|
||||
|
@@ -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,26 +23,13 @@ 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
|
||||
@@ -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
|
||||
|
@@ -19,15 +19,6 @@
|
||||
## 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
|
||||
|
||||
|
||||
|
@@ -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:
|
||||
import distro
|
||||
|
||||
from jellypy import version
|
||||
|
||||
|
||||
# Identify Our Application
|
||||
PRODUCT = 'Tautulli'
|
||||
PRODUCT = 'JellPy'
|
||||
PLATFORM = platform.system()
|
||||
PLATFORM_RELEASE = platform.release()
|
||||
PLATFORM_VERSION = platform.version()
|
||||
@@ -329,287 +324,527 @@ 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': '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': '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': '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': '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': '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': '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': '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': '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': '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': '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': '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 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': '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': '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': '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': '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.'},
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -618,31 +853,50 @@ 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.'},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -13,23 +13,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 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
|
||||
|
||||
@@ -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', ''),
|
||||
|
@@ -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 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
|
||||
|
||||
|
||||
FILENAME = "tautulli.db"
|
||||
db_lock = threading.Lock()
|
||||
|
||||
@@ -455,7 +447,8 @@ 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())) + ")" +
|
||||
"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:
|
||||
|
@@ -15,25 +15,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 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
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -867,7 +863,8 @@ class DataFactory(object):
|
||||
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,
|
||||
@@ -1619,15 +1616,19 @@ 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 = ?',
|
||||
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 = ?',
|
||||
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 = ?',
|
||||
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 = ?',
|
||||
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)
|
||||
@@ -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 []
|
||||
|
@@ -13,17 +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
|
||||
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
|
||||
|
@@ -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
|
||||
"""
|
||||
|
@@ -14,30 +14,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 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
|
||||
|
@@ -15,21 +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 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
|
||||
@@ -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',
|
||||
@@ -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 = []
|
||||
@@ -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 = []
|
||||
|
@@ -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,17 +30,23 @@ 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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
|
@@ -14,23 +14,14 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
|
@@ -15,27 +15,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
|
||||
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
|
||||
@@ -378,7 +362,8 @@ 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'],
|
||||
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)
|
||||
@@ -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 = {}
|
||||
@@ -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:
|
||||
@@ -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)
|
||||
|
||||
@@ -742,14 +732,16 @@ 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)
|
||||
@@ -758,6 +750,7 @@ class Libraries(object):
|
||||
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,7 +849,8 @@ class Libraries(object):
|
||||
return library_details
|
||||
|
||||
else:
|
||||
logger.warn("Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
|
||||
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()
|
||||
|
@@ -14,18 +14,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 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
|
||||
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
||||
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"""
|
||||
|
@@ -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,16 +25,16 @@ 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
|
||||
|
||||
|
||||
# These settings are for file logging only
|
||||
FILENAME = "tautulli.log"
|
||||
FILENAME_API = "tautulli_api.log"
|
||||
@@ -78,6 +72,7 @@ class NoThreadFilter(logging.Filter):
|
||||
"""
|
||||
Log filter for the current thread
|
||||
"""
|
||||
|
||||
def __init__(self, threadName):
|
||||
super(NoThreadFilter, self).__init__()
|
||||
|
||||
@@ -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
|
||||
|
@@ -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,11 +32,7 @@ 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
|
||||
|
@@ -15,23 +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 requests
|
||||
import threading
|
||||
|
||||
import jellypy
|
||||
if jellypy.PYTHON2:
|
||||
import database
|
||||
import helpers
|
||||
import logger
|
||||
else:
|
||||
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}
|
||||
|
@@ -15,27 +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 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
|
||||
|
||||
|
||||
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)
|
||||
|
@@ -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 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
|
||||
@@ -49,7 +35,6 @@ else:
|
||||
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():
|
||||
@@ -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():
|
||||
|
@@ -15,43 +15,23 @@
|
||||
# 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
|
||||
@@ -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:
|
||||
|
||||
@@ -291,7 +280,8 @@ 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'."
|
||||
logger.error(
|
||||
"Tautulli NotificationHandler :: {%s} Unable to cast condition '%s', values '%s', to type '%s'."
|
||||
% (i + 1, parameter, values, parameter_type))
|
||||
return False
|
||||
|
||||
@@ -307,7 +297,8 @@ 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'."
|
||||
logger.error(
|
||||
"Tautulli NotificationHandler :: {%s} Unable to cast parameter '%s', value '%s', to type '%s'."
|
||||
% (i + 1, parameter, parameter_value, parameter_type))
|
||||
return False
|
||||
|
||||
@@ -343,7 +334,8 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||
|
||||
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
|
||||
@@ -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']
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -15,21 +15,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 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:
|
||||
import arrow
|
||||
|
||||
from jellypy import activity_processor
|
||||
from jellypy import database
|
||||
from jellypy import helpers
|
||||
@@ -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
|
||||
|
@@ -15,24 +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 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
|
||||
@@ -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)
|
||||
@@ -833,8 +820,10 @@ class PlexTV(object):
|
||||
% 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."
|
||||
|
@@ -15,20 +15,9 @@
|
||||
# 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
|
||||
@@ -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
|
||||
|
@@ -15,29 +15,12 @@
|
||||
# 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
|
||||
@@ -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)
|
||||
@@ -1217,7 +1201,8 @@ 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,
|
||||
'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'
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -15,25 +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 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
|
||||
|
||||
|
||||
# Dictionary with last request times, for rate limiting.
|
||||
last_requests = collections.defaultdict(int)
|
||||
fake_lock = lock.FakeLock()
|
||||
|
@@ -14,17 +14,9 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
@@ -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,6 +131,7 @@ 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
|
||||
@@ -147,6 +146,7 @@ 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
|
||||
@@ -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
|
||||
|
@@ -14,26 +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 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
|
||||
@@ -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()
|
||||
|
@@ -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"
|
||||
|
@@ -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,12 +23,6 @@ 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
|
||||
@@ -41,7 +30,6 @@ else:
|
||||
|
||||
|
||||
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()
|
||||
|
@@ -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,20 +26,12 @@ 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
|
||||
|
||||
|
||||
name = 'websocket'
|
||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||
ws_shutdown = False
|
||||
|
@@ -20,23 +20,15 @@
|
||||
# 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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
@@ -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,55 +24,18 @@ 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
|
||||
@@ -108,6 +65,7 @@ else:
|
||||
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':
|
||||
@@ -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")
|
||||
@@ -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"))
|
||||
@@ -1807,7 +1763,6 @@ class WebInterface(object):
|
||||
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"))
|
||||
@@ -3047,7 +3000,6 @@ class WebInterface(object):
|
||||
except IOError as e:
|
||||
return "Log file not found."
|
||||
|
||||
|
||||
##### Settings #####
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -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.'}
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
@@ -21,12 +21,6 @@ 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
|
||||
@@ -64,7 +58,6 @@ def restart():
|
||||
|
||||
|
||||
def initialize(options):
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
enable_https = options['enable_https']
|
||||
https_cert = options['https_cert']
|
||||
@@ -238,7 +231,8 @@ def initialize(options):
|
||||
# },
|
||||
'/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,
|
||||
|
Reference in New Issue
Block a user