More rename, more -python2

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

View File

@@ -21,9 +21,6 @@ import sys
import threading import threading
import uuid 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 # Some cut down versions of Python may not include this module and it's not critical for us
try: try:
import webbrowser import webbrowser
@@ -528,19 +525,6 @@ def start():
# Cancel processing exports # Cancel processing exports
exporter.cancel_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 _STARTED = True

View File

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

View File

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

View File

@@ -13,28 +13,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import str
from future.builtins import object
from collections import defaultdict
import json import json
from collections import defaultdict
import jellypy import jellypy
if jellypy.PYTHON2:
import database from jellypy import database
import helpers from jellypy import helpers
import libraries from jellypy import libraries
import logger from jellypy import logger
import pmsconnect from jellypy import pmsconnect
import users from jellypy import users
else:
from jellypy import database
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import pmsconnect
from jellypy import users
class ActivityProcessor(object): class ActivityProcessor(object):
@@ -229,7 +218,8 @@ class ActivityProcessor(object):
(session['session_key'], session['rating_key'], session['media_type'])) (session['session_key'], session['rating_key'], session['media_type']))
return session['id'] 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 not is_import and jellypy.CONFIG.LOGGING_IGNORE_INTERVAL:
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ 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 \ if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
(real_play_time < int(import_ignore_interval)): (real_play_time < int(import_ignore_interval)):
logging_enabled = False logging_enabled = False
logger.debug("Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s " logger.debug(
"seconds, so we're not logging it." % "Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
(session['rating_key'], str(real_play_time), import_ignore_interval)) "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']: if not is_import and not user_details['keep_history']:
logging_enabled = False 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']: elif not is_import and not library_details['keep_history']:
logging_enabled = False 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: if logging_enabled:
# Fetch metadata first so we can return false if it fails # Fetch metadata first so we can return false if it fails
if not is_import: 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() pms_connect = pmsconnect.PmsConnect()
if session['live']: if session['live']:
metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']), metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']),

View File

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

View File

@@ -19,16 +19,7 @@
## Stolen from Sick-Beard's classes.py ## ## Stolen from Sick-Beard's classes.py ##
######################################### #########################################
from jellypy.common import USER_AGENT
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
class PlexPyURLopener(FancyURLopener): class PlexPyURLopener(FancyURLopener):

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,20 +13,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import object
import re import re
import jellypy from jellypy import database
if jellypy.PYTHON2: from jellypy import helpers
import database from jellypy import logger
import helpers
import logger
else:
from jellypy import database
from jellypy import helpers
from jellypy import logger
class DataTables(object): class DataTables(object):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,38 +15,21 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from future.builtins import next
from future.builtins import str
from future.builtins import object
import json import json
import os import os
import time from urllib.parse import quote_plus, quote, urlencode
from future.moves.urllib.parse import quote, quote_plus, urlencode
from xml.dom.minidom import Node from xml.dom.minidom import Node
import jellypy import jellypy
if jellypy.PYTHON2: from jellypy import activity_processor
import activity_processor from jellypy import common
import common from jellypy import helpers
import helpers from jellypy import http_handler
import http_handler from jellypy import libraries
import libraries from jellypy import logger
import logger from jellypy import plextv
import plextv from jellypy import session
import session from jellypy import users
import users
else:
from jellypy import activity_processor
from jellypy import common
from jellypy import helpers
from jellypy import http_handler
from jellypy import libraries
from jellypy import logger
from jellypy import plextv
from jellypy import session
from jellypy import users
def get_server_friendly_name(): def get_server_friendly_name():
@@ -217,7 +200,8 @@ class PmsConnect(object):
Output: array 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 = self.request_handler.make_request(uri=uri,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -474,7 +458,7 @@ class PmsConnect(object):
return request return request
def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''): def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''):
""" """
Return processed and validated list of recently added items. Return processed and validated list of recently added items.
@@ -889,7 +873,7 @@ class PmsConnect(object):
'collections': show_details.get('collections', []), 'collections': show_details.get('collections', []),
'guids': show_details.get('guids', []), 'guids': show_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')), helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
} }
@@ -962,7 +946,7 @@ class PmsConnect(object):
'collections': show_details.get('collections', []), 'collections': show_details.get('collections', []),
'guids': show_details.get('guids', []), 'guids': show_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
helpers.get_xml_attr(metadata_main, 'title')), helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
} }
@@ -1063,7 +1047,7 @@ class PmsConnect(object):
'collections': collections, 'collections': collections,
'guids': guids, 'guids': guids,
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
helpers.get_xml_attr(metadata_main, 'title')), helpers.get_xml_attr(metadata_main, 'title')),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
} }
@@ -1117,7 +1101,7 @@ class PmsConnect(object):
'collections': album_details.get('collections', []), 'collections': album_details.get('collections', []),
'guids': album_details.get('guids', []), 'guids': album_details.get('guids', []),
'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'),
track_artist), track_artist),
'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1')
} }
@@ -1217,8 +1201,9 @@ class PmsConnect(object):
'labels': photo_album_details.get('labels', []), 'labels': photo_album_details.get('labels', []),
'collections': photo_album_details.get('collections', []), 'collections': photo_album_details.get('collections', []),
'guids': photo_album_details.get('guids', []), '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, 'title')), 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')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')),
'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') '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_codec_level': helpers.get_xml_attr(stream, 'level'),
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'), 'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'), '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_primaries': helpers.get_xml_attr(stream, 'colorPrimaries'),
'video_color_range': helpers.get_xml_attr(stream, 'colorRange'), 'video_color_range': helpers.get_xml_attr(stream, 'colorRange'),
'video_color_space': helpers.get_xml_attr(stream, 'colorSpace'), '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_container': helpers.get_xml_attr(stream, 'container'),
'subtitle_format': helpers.get_xml_attr(stream, 'format'), 'subtitle_format': helpers.get_xml_attr(stream, 'format'),
'subtitle_forced': int(helpers.get_xml_attr(stream, 'forced') == '1'), 'subtitle_forced': int(helpers.get_xml_attr(stream, 'forced') == '1'),
'subtitle_location': 'external' if helpers.get_xml_attr(stream, 'key') else 'embedded', 'subtitle_location': 'external' if helpers.get_xml_attr(stream,
'key') else 'embedded',
'subtitle_language': helpers.get_xml_attr(stream, 'language'), 'subtitle_language': helpers.get_xml_attr(stream, 'language'),
'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode'), 'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode'),
'selected': int(helpers.get_xml_attr(stream, 'selected') == '1') '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') 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], 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'), 'device': helpers.get_xml_attr(player_info, 'device'),
'platform': platform, 'platform': platform,
'platform_name': platform_name, 'platform_name': platform_name,
@@ -1681,7 +1669,8 @@ class PmsConnect(object):
'product': helpers.get_xml_attr(player_info, 'product'), 'product': helpers.get_xml_attr(player_info, 'product'),
'product_version': helpers.get_xml_attr(player_info, 'version'), 'product_version': helpers.get_xml_attr(player_info, 'version'),
'profile': helpers.get_xml_attr(player_info, 'profile'), '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'), 'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier'),
'state': helpers.get_xml_attr(player_info, 'state'), 'state': helpers.get_xml_attr(player_info, 'state'),
'local': int(helpers.get_xml_attr(player_info, 'local') == '1'), '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_channels': helpers.get_xml_attr(transcode_info, 'audioChannels'),
'transcode_audio_codec': helpers.get_xml_attr(transcode_info, 'audioCodec'), 'transcode_audio_codec': helpers.get_xml_attr(transcode_info, 'audioCodec'),
'transcode_video_codec': helpers.get_xml_attr(transcode_info, 'videoCodec'), '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_width': helpers.get_xml_attr(transcode_info, 'width'),
'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), # Blank but keep backwards compatibility # 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_container': helpers.get_xml_attr(transcode_info, 'container'),
'transcode_protocol': helpers.get_xml_attr(transcode_info, 'protocol'), 'transcode_protocol': helpers.get_xml_attr(transcode_info, 'protocol'),
'transcode_hw_requested': 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': helpers.get_xml_attr(transcode_info, 'transcodeHwDecoding'),
'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwDecodingTitle'), 'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info,
'transcodeHwDecodingTitle'),
'transcode_hw_encode': helpers.get_xml_attr(transcode_info, 'transcodeHwEncoding'), 'transcode_hw_encode': helpers.get_xml_attr(transcode_info, 'transcodeHwEncoding'),
'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwEncodingTitle'), 'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info,
'transcode_hw_full_pipeline': int(helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1'), 'transcodeHwEncodingTitle'),
'transcode_hw_full_pipeline': int(
helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1'),
'audio_decision': helpers.get_xml_attr(transcode_info, 'audioDecision'), 'audio_decision': helpers.get_xml_attr(transcode_info, 'audioDecision'),
'video_decision': helpers.get_xml_attr(transcode_info, 'videoDecision'), 'video_decision': helpers.get_xml_attr(transcode_info, 'videoDecision'),
'subtitle_decision': helpers.get_xml_attr(transcode_info, 'subtitleDecision'), 'subtitle_decision': helpers.get_xml_attr(transcode_info, 'subtitleDecision'),
'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: else:
transcode_session = False transcode_session = False
@@ -1773,8 +1769,10 @@ class PmsConnect(object):
} }
# Check HW decoding/encoding # Check HW decoding/encoding
transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS) transcode_details['transcode_hw_decoding'] = int(
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS) 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 # Determine if a synced version is being played
sync_id = synced_session_data = synced_item_details = None 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') grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'], 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: if synced_items:
synced_item_details = synced_items[0] synced_item_details = synced_items[0]
sync_id = synced_item_details['sync_id'] sync_id = synced_item_details['sync_id']
@@ -1810,9 +1809,11 @@ class PmsConnect(object):
media_info_all = synced_session_data.getElementsByTagName('Media') media_info_all = synced_session_data.getElementsByTagName('Media')
else: else:
media_info_all = session.getElementsByTagName('Media') 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') 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 # Get the stream details
video_stream_info = audio_stream_info = subtitle_stream_info = None 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_id = helpers.get_xml_attr(video_stream_info, 'id')
video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'), 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_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_primaries': helpers.get_xml_attr(video_stream_info, 'colorPrimaries'),
'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'), 'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'),
'stream_video_color_space': helpers.get_xml_attr(video_stream_info, 'colorSpace'), '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': helpers.get_xml_attr(video_stream_info, 'language'),
'stream_video_language_code': helpers.get_xml_attr(video_stream_info, 'languageCode'), '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_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: else:
video_details = {'stream_video_bitrate': '', 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'), 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_bitrate_mode': helpers.get_xml_attr(audio_stream_info, 'bitrateMode'),
'stream_audio_sample_rate': helpers.get_xml_attr(audio_stream_info, 'samplingRate'), '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': helpers.get_xml_attr(audio_stream_info, 'language'),
'stream_audio_language_code': helpers.get_xml_attr(audio_stream_info, 'languageCode'), '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: else:
audio_details = {'stream_audio_bitrate': '', 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'), subtitle_details = {'stream_subtitle_codec': helpers.get_xml_attr(subtitle_stream_info, 'codec'),
'stream_subtitle_container': helpers.get_xml_attr(subtitle_stream_info, 'container'), 'stream_subtitle_container': helpers.get_xml_attr(subtitle_stream_info, 'container'),
'stream_subtitle_format': helpers.get_xml_attr(subtitle_stream_info, 'format'), 'stream_subtitle_format': helpers.get_xml_attr(subtitle_stream_info, 'format'),
'stream_subtitle_forced': 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_location': helpers.get_xml_attr(subtitle_stream_info, 'location'),
'stream_subtitle_language': helpers.get_xml_attr(subtitle_stream_info, 'language'), 'stream_subtitle_language': helpers.get_xml_attr(subtitle_stream_info, 'language'),
'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info, 'languageCode'), 'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info,
'languageCode'),
'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision'), '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: else:
subtitle_selected = None subtitle_selected = None
@@ -1913,7 +1921,8 @@ class PmsConnect(object):
view_offset = helpers.get_xml_attr(session, 'viewOffset') view_offset = helpers.get_xml_attr(session, 'viewOffset')
if indexes == 'sd': if indexes == 'sd':
part_id = helpers.get_xml_attr(stream_media_parts_info, 'id') 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: else:
bif_thumb = '' bif_thumb = ''
@@ -1930,14 +1939,19 @@ class PmsConnect(object):
'stream_aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'), 'stream_aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'),
'stream_audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'), 'stream_audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'),
'stream_audio_channels': stream_audio_channels, '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_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'stream_video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'), 'stream_video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'),
'stream_video_resolution': stream_video_resolution, 'stream_video_resolution': stream_video_resolution,
'stream_video_height': helpers.get_xml_attr(stream_media_info, 'height'), 'stream_video_height': helpers.get_xml_attr(stream_media_info, 'height'),
'stream_video_width': helpers.get_xml_attr(stream_media_info, 'width'), '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_duration': helpers.get_xml_attr(stream_media_info,
'stream_container_decision': 'direct play' if sync_id else helpers.get_xml_attr(stream_media_parts_info, 'decision').replace('directplay', 'direct play'), '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': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'),
'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'), 'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'),
'synced_version': 1 if sync_id else 0, 'synced_version': 1 if sync_id else 0,
@@ -2004,7 +2018,8 @@ class PmsConnect(object):
'aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'), 'aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'),
'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'), 'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'),
'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(), '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_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'),
'video_profile': helpers.get_xml_attr(stream_media_info, 'videoProfile'), 'video_profile': helpers.get_xml_attr(stream_media_info, 'videoProfile'),
'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'), '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 # Get the media info, fallback to first item if match id is not found
source_medias = metadata_details.pop('media_info', []) source_medias = metadata_details.pop('media_info', [])
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_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_media_part_streams = source_media_part_details.pop('streams', [])
source_video_details = {'id': '', source_video_details = {'id': '',
@@ -2080,13 +2097,16 @@ class PmsConnect(object):
} }
if video_id: if video_id:
source_video_details = next((p for p in source_media_part_streams if p['id'] == 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: if audio_id:
source_audio_details = next((p for p in source_media_part_streams if p['id'] == 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: if subtitle_id:
source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == 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 # Override the thumb for clips
if media_type == 'clip' and metadata_details.get('extra_type') and metadata_details['art']: 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']) transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels'])
# Generate a combined transcode decision value # 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' transcode_decision = 'transcode'
elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy': elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy':
transcode_decision = 'copy' transcode_decision = 'copy'
@@ -2198,7 +2219,8 @@ class PmsConnect(object):
stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate']) stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate'])
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate')) source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
try: 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] quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate]
except ValueError: except ValueError:
quality_profile = 'Original' quality_profile = 'Original'
@@ -2463,7 +2485,7 @@ class PmsConnect(object):
'thumb': helpers.get_xml_attr(result, 'thumb'), 'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'), 'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration') 'duration': helpers.get_xml_attr(result, 'duration')
} }
children_results_list[media_type].append(children_output) children_results_list[media_type].append(children_output)
output = {'results_count': sum(len(v) for k, v in children_results_list.items()), output = {'results_count': sum(len(v) for k, v in children_results_list.items()),
@@ -2630,11 +2652,13 @@ class PmsConnect(object):
sort_type = '' sort_type = ''
if str(section_id).isdigit(): 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(): elif str(rating_key).isdigit():
library_data = self.get_metadata_children(str(rating_key), output_format='xml') library_data = self.get_metadata_children(str(rating_key), output_format='xml')
else: 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 [] return []
try: try:
@@ -2735,7 +2759,8 @@ class PmsConnect(object):
for library in libraries_list: for library in libraries_list:
section_type = library['section_type'] section_type = library['section_type']
section_id = library['section_id'] 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: if children_list:
library_stats = {'section_id': section_id, library_stats = {'section_id': section_id,
@@ -2749,34 +2774,40 @@ class PmsConnect(object):
} }
if section_type == 'show': 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: if parent_list:
parent_stats = {'parent_count': parent_list['library_count']} parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats) 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: if child_list:
child_stats = {'child_count': child_list['library_count']} child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats) library_stats.update(child_stats)
if section_type == 'artist': 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: if parent_list:
parent_stats = {'parent_count': parent_list['library_count']} parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats) 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: if child_list:
child_stats = {'child_count': child_list['library_count']} child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats) library_stats.update(child_stats)
if section_type == 'photo': 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: if parent_list:
parent_stats = {'parent_count': parent_list['library_count']} parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats) 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: if child_list:
child_stats = {'child_count': child_list['library_count']} child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats) library_stats.update(child_stats)
@@ -2896,7 +2927,7 @@ class PmsConnect(object):
for h in hubs: for h in hubs:
if helpers.get_xml_attr(h, 'size') == '0' or \ if helpers.get_xml_attr(h, 'size') == '0' or \
helpers.get_xml_attr(h, 'type') not in search_results_list: helpers.get_xml_attr(h, 'type') not in search_results_list:
continue continue
if h.getElementsByTagName('Video'): if h.getElementsByTagName('Video'):
@@ -2971,7 +3002,8 @@ class PmsConnect(object):
section_id = metadata['section_id'] section_id = metadata['section_id']
library_name = metadata['library_name'] library_name = metadata['library_name']
except Exception as e: 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 {} return {}
# get parent_rating_keys # get parent_rating_keys
@@ -3035,8 +3067,8 @@ class PmsConnect(object):
key = int(parent_index) if match_type == 'index' else parent_title key = int(parent_index) if match_type == 'index' else parent_title
parents.update({key: parents.update({key:
{'rating_key': int(parent_rating_key), {'rating_key': int(parent_rating_key),
'children': children} 'children': children}
}) })
key = 0 if match_type == 'index' else title key = 0 if match_type == 'index' else title
@@ -3097,7 +3129,8 @@ class PmsConnect(object):
# Catch the malformed XML on certain PMX version. # Catch the malformed XML on certain PMX version.
# XML parser helper returns empty list if there is an error parsing XML # XML parser helper returns empty list if there is an error parsing XML
if updater_status == []: 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.") logger.info("Tautulli is unable to check for Plex updates. Disabling check for Plex updates.")
# Disable check for Plex updates # Disable check for Plex updates

View File

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

View File

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

View File

@@ -14,34 +14,19 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from urllib.parse import parse_qsl
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
import httpagentparser import httpagentparser
import jellypy import jellypy
if jellypy.PYTHON2: from jellypy import common
import common from jellypy import database
import database from jellypy import datatables
import datatables from jellypy import helpers
import helpers from jellypy import libraries
import libraries from jellypy import logger
import logger from jellypy import plextv
import plextv from jellypy import session
import session
else:
from jellypy import common
from jellypy import database
from jellypy import datatables
from jellypy import helpers
from jellypy import libraries
from jellypy import logger
from jellypy import plextv
from jellypy import session
def refresh_users(): def refresh_users():
@@ -847,7 +832,8 @@ class Users(object):
return filters_list 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(): if user_id is None or str(user_id).isdigit():
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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