diff --git a/jellypy/__init__.py b/jellypy/__init__.py index dd912be8..e50710bb 100644 --- a/jellypy/__init__.py +++ b/jellypy/__init__.py @@ -21,9 +21,6 @@ import sys import threading import uuid -import future.moves.queue as queue -from future.builtins import range - # Some cut down versions of Python may not include this module and it's not critical for us try: import webbrowser @@ -528,19 +525,6 @@ def start(): # Cancel processing exports exporter.cancel_exports() - if CONFIG.SYSTEM_ANALYTICS: - global TRACKER - TRACKER = initialize_tracker() - - # Send system analytics events - if not CONFIG.FIRST_RUN_COMPLETE: - analytics_event(category='system', action='install') - - elif _UPDATE: - analytics_event(category='system', action='update') - - analytics_event(category='system', action='start') - _STARTED = True diff --git a/jellypy/activity_handler.py b/jellypy/activity_handler.py index 19306958..e19488f8 100644 --- a/jellypy/activity_handler.py +++ b/jellypy/activity_handler.py @@ -13,33 +13,21 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import object - import datetime import os import time -from apscheduler.triggers.date import DateTrigger import pytz +from apscheduler.triggers.date import DateTrigger import jellypy -if jellypy.PYTHON2: - import activity_processor - import datafactory - import helpers - import logger - import notification_handler - import pmsconnect -else: - from jellypy import activity_processor - from jellypy import datafactory - from jellypy import helpers - from jellypy import logger - from jellypy import notification_handler - from jellypy import pmsconnect +from jellypy import activity_processor +from jellypy import datafactory +from jellypy import helpers +from jellypy import logger +from jellypy import notification_handler +from jellypy import pmsconnect ACTIVITY_SCHED = None @@ -134,7 +122,7 @@ class ActivityHandler(object): str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else '')) # Send notification after updating db - #jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'}) + # jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'}) # Write the new session to our temp session table self.update_db_session(session=session, notify=True) @@ -218,7 +206,8 @@ class ActivityHandler(object): def on_change(self): if self.is_valid_session(): - logger.debug("Tautulli ActivityHandler :: Session %s has changed transcode decision." % str(self.get_session_key())) + logger.debug( + "Tautulli ActivityHandler :: Session %s has changed transcode decision." % str(self.get_session_key())) # Update the session state and viewOffset self.update_db_session() @@ -316,8 +305,8 @@ class ActivityHandler(object): # Make sure the same item is being played if (this_rating_key == last_rating_key - or this_rating_key == last_rating_key_websocket - or this_live_uuid == last_live_uuid) \ + or this_rating_key == last_rating_key_websocket + or this_live_uuid == last_live_uuid) \ and this_guid == last_guid: # Update the session state and viewOffset if this_state == 'playing': @@ -374,8 +363,8 @@ class ActivityHandler(object): for d in watched_notifiers: jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), - 'notifier_id': d['notifier_id'], - 'notify_action': 'on_watched'}) + 'notifier_id': d['notifier_id'], + 'notify_action': 'on_watched'}) else: # We don't have this session in our table yet, start a new one. @@ -439,9 +428,9 @@ class TimelineHandler(object): # Add a new media item to the recently added queue if media_type and section_id > 0 and \ - ((state_type == 0 and metadata_state == 'created')): # or \ - #(jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \ - #media_state == 'analyzing' and queue_size is None)): + ((state_type == 0 and metadata_state == 'created')): # or \ + # (jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \ + # media_state == 'analyzing' and queue_size is None)): if media_type in ('episode', 'track'): metadata = self.get_metadata() @@ -460,8 +449,9 @@ class TimelineHandler(object): RECENTLY_ADDED_QUEUE[rating_key] = set([grandparent_rating_key]) - logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s, grandparent %s) added to recently added queue." - % (title, str(rating_key), str(grandparent_rating_key))) + logger.debug( + "Tautulli TimelineHandler :: Library item '%s' (%s, grandparent %s) added to recently added queue." + % (title, str(rating_key), str(grandparent_rating_key))) # Schedule a callback to clear the recently added queue schedule_callback('rating_key-{}'.format(grandparent_rating_key), @@ -479,8 +469,9 @@ class TimelineHandler(object): parent_set.add(rating_key) RECENTLY_ADDED_QUEUE[parent_rating_key] = parent_set - logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s , parent %s) added to recently added queue." - % (title, str(rating_key), str(parent_rating_key))) + logger.debug( + "Tautulli TimelineHandler :: Library item '%s' (%s , parent %s) added to recently added queue." + % (title, str(rating_key), str(parent_rating_key))) # Schedule a callback to clear the recently added queue schedule_callback('rating_key-{}'.format(parent_rating_key), @@ -503,8 +494,8 @@ class TimelineHandler(object): # A movie, show, or artist is done processing elif media_type in ('movie', 'show', 'artist') and section_id > 0 and \ - state_type == 5 and metadata_state is None and queue_size is None and \ - rating_key in RECENTLY_ADDED_QUEUE: + state_type == 5 and metadata_state is None and queue_size is None and \ + rating_key in RECENTLY_ADDED_QUEUE: logger.debug("Tautulli TimelineHandler :: Library item '%s' (%s) done processing metadata." % (title, str(rating_key))) @@ -618,8 +609,9 @@ def force_stop_stream(session_key, title, user): if row_id: # If session is written to the database successfully, remove the session from the session table - logger.info("Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" - % (session['session_key'], session['rating_key'])) + logger.info( + "Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" + % (session['session_key'], session['rating_key'])) ap.delete_session(row_id=row_id) delete_metadata_cache(session_key) @@ -627,9 +619,10 @@ def force_stop_stream(session_key, title, user): session['write_attempts'] += 1 if session['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS: - logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ - "Will try again in 30 seconds. Write attempt %s." - % (session['session_key'], session['rating_key'], str(session['write_attempts']))) + logger.warn( + "Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ + "Will try again in 30 seconds. Write attempt %s." + % (session['session_key'], session['rating_key'], str(session['write_attempts']))) ap.increment_write_attempts(session_key=session_key) # Reschedule for 30 seconds later @@ -637,11 +630,13 @@ def force_stop_stream(session_key, title, user): args=[session_key, session['full_title'], session['user']], seconds=30) else: - logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ - "Removing session from the database. Write attempt %s." - % (session['session_key'], session['rating_key'], str(session['write_attempts']))) - logger.info("Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" - % (session['session_key'], session['rating_key'])) + logger.warn( + "Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ + "Removing session from the database. Write attempt %s." + % (session['session_key'], session['rating_key'], str(session['write_attempts']))) + logger.info( + "Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" + % (session['session_key'], session['rating_key'])) ap.delete_session(session_key=session_key) delete_metadata_cache(session_key) diff --git a/jellypy/activity_pinger.py b/jellypy/activity_pinger.py index d8b01898..a692e964 100644 --- a/jellypy/activity_pinger.py +++ b/jellypy/activity_pinger.py @@ -13,35 +13,19 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str - import threading import jellypy -if jellypy.PYTHON2: - import activity_handler - import activity_processor - import database - import helpers - import libraries - import logger - import notification_handler - import plextv - import pmsconnect - import web_socket -else: - from jellypy import activity_handler - from jellypy import activity_processor - from jellypy import database - from jellypy import helpers - from jellypy import libraries - from jellypy import logger - from jellypy import notification_handler - from jellypy import plextv - from jellypy import pmsconnect - from jellypy import web_socket +from jellypy import activity_handler +from jellypy import activity_processor +from jellypy import database +from jellypy import helpers +from jellypy import logger +from jellypy import notification_handler +from jellypy import plextv +from jellypy import pmsconnect +from jellypy import web_socket monitor_lock = threading.Lock() ext_ping_count = 0 @@ -50,7 +34,6 @@ int_ping_count = 0 def check_active_sessions(ws_request=False): - with monitor_lock: monitor_db = database.MonitorDatabase() monitor_process = activity_processor.ActivityProcessor() @@ -82,17 +65,21 @@ def check_active_sessions(ws_request=False): if session['state'] == 'paused': logger.debug("Tautulli Monitor :: Session %s paused." % stream['session_key']) - jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'}) + jellypy.NOTIFY_QUEUE.put( + {'stream_data': stream.copy(), 'notify_action': 'on_pause'}) if session['state'] == 'playing' and stream['state'] == 'paused': logger.debug("Tautulli Monitor :: Session %s resumed." % stream['session_key']) - jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'}) + jellypy.NOTIFY_QUEUE.put( + {'stream_data': stream.copy(), 'notify_action': 'on_resume'}) if session['state'] == 'error': - logger.debug("Tautulli Monitor :: Session %s encountered an error." % stream['session_key']) + logger.debug( + "Tautulli Monitor :: Session %s encountered an error." % stream['session_key']) - jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_error'}) + jellypy.NOTIFY_QUEUE.put( + {'stream_data': stream.copy(), 'notify_action': 'on_error'}) if stream['state'] == 'paused' and not ws_request: # The stream is still paused so we need to increment the paused_counter @@ -130,26 +117,30 @@ def check_active_sessions(ws_request=False): 'WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) - jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'}) + jellypy.NOTIFY_QUEUE.put( + {'stream_data': stream.copy(), 'notify_action': 'on_buffer'}) else: # Subsequent buffer notifications after wait time if helpers.timestamp() > buffer_values[0]['buffer_last_triggered'] + \ jellypy.CONFIG.BUFFER_WAIT: - logger.info("Tautulli Monitor :: User '%s' has triggered multiple buffer warnings." - % stream['user']) + logger.info( + "Tautulli Monitor :: User '%s' has triggered multiple buffer warnings." + % stream['user']) # Set the buffer trigger time monitor_db.action('UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) - jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'}) + jellypy.NOTIFY_QUEUE.put( + {'stream_data': stream.copy(), 'notify_action': 'on_buffer'}) - logger.debug("Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s." - % (stream['session_key'], - buffer_values[0]['buffer_count'], - buffer_values[0]['buffer_last_triggered'])) + logger.debug( + "Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s." + % (stream['session_key'], + buffer_values[0]['buffer_count'], + buffer_values[0]['buffer_last_triggered'])) # Check if the user has reached the offset in the media we defined as the "watched" percent # Don't trigger if state is buffer as some clients push the progress to the end when @@ -157,11 +148,15 @@ def check_active_sessions(ws_request=False): if session['state'] != 'buffering': progress_percent = helpers.get_percent(session['view_offset'], session['duration']) notify_states = notification_handler.get_notify_state(session=session) - if (session['media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or - session['media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or - session['media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \ - and not any(d['notify_action'] == 'on_watched' for d in notify_states): - jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'}) + if (session[ + 'media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or + session[ + 'media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or + session[ + 'media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \ + and not any(d['notify_action'] == 'on_watched' for d in notify_states): + jellypy.NOTIFY_QUEUE.put( + {'stream_data': stream.copy(), 'notify_action': 'on_watched'}) else: # The user has stopped playing a stream @@ -173,14 +168,18 @@ def check_active_sessions(ws_request=False): stream['stopped'] = helpers.timestamp() monitor_db.action('UPDATE sessions SET stopped = ?, state = ? ' 'WHERE session_key = ? AND rating_key = ?', - [stream['stopped'], 'stopped', stream['session_key'], stream['rating_key']]) + [stream['stopped'], 'stopped', stream['session_key'], + stream['rating_key']]) progress_percent = helpers.get_percent(stream['view_offset'], stream['duration']) notify_states = notification_handler.get_notify_state(session=stream) - if (stream['media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or - stream['media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or - stream['media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \ - and not any(d['notify_action'] == 'on_watched' for d in notify_states): + if (stream[ + 'media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or + stream[ + 'media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or + stream[ + 'media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \ + and not any(d['notify_action'] == 'on_watched' for d in notify_states): jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'}) jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'}) @@ -197,14 +196,16 @@ def check_active_sessions(ws_request=False): stream['write_attempts'] += 1 if stream['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS: - logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ - "Will try again on the next pass. Write attempt %s." - % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) + logger.warn( + "Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ + "Will try again on the next pass. Write attempt %s." + % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) monitor_process.increment_write_attempts(session_key=stream['session_key']) else: - logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ - "Removing session from the database. Write attempt %s." - % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) + logger.warn( + "Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ + "Removing session from the database. Write attempt %s." + % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) logger.debug("Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue" % (stream['session_key'], stream['rating_key'])) monitor_process.delete_session(session_key=stream['session_key']) @@ -216,7 +217,8 @@ def check_active_sessions(ws_request=False): if new_session: logger.debug("Tautulli Monitor :: Session %s started by user %s (%s) with ratingKey %s (%s)%s." % (str(session['session_key']), str(session['user_id']), session['username'], - str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else '')) + str(session['rating_key']), session['full_title'], + '[Live TV]' if session['live'] else '')) else: logger.debug("Tautulli Monitor :: Unable to read session list.") @@ -256,7 +258,6 @@ def connect_server(log=True, startup=False): def check_server_updates(): - with monitor_lock: logger.info("Tautulli Monitor :: Checking for PMS updates...") diff --git a/jellypy/activity_processor.py b/jellypy/activity_processor.py index 3aece292..d20c12d4 100644 --- a/jellypy/activity_processor.py +++ b/jellypy/activity_processor.py @@ -13,28 +13,17 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import object - -from collections import defaultdict import json +from collections import defaultdict import jellypy -if jellypy.PYTHON2: - import database - import helpers - import libraries - import logger - import pmsconnect - import users -else: - from jellypy import database - from jellypy import helpers - from jellypy import libraries - from jellypy import logger - from jellypy import pmsconnect - from jellypy import users + +from jellypy import database +from jellypy import helpers +from jellypy import libraries +from jellypy import logger +from jellypy import pmsconnect +from jellypy import users class ActivityProcessor(object): @@ -229,7 +218,8 @@ class ActivityProcessor(object): (session['session_key'], session['rating_key'], session['media_type'])) return session['id'] - real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(session['paused_counter']) + real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int( + session['paused_counter']) if not is_import and jellypy.CONFIG.LOGGING_IGNORE_INTERVAL: if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ @@ -249,22 +239,27 @@ class ActivityProcessor(object): if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ (real_play_time < int(import_ignore_interval)): logging_enabled = False - logger.debug("Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s " - "seconds, so we're not logging it." % - (session['rating_key'], str(real_play_time), import_ignore_interval)) + logger.debug( + "Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s " + "seconds, so we're not logging it." % + (session['rating_key'], str(real_play_time), import_ignore_interval)) if not is_import and not user_details['keep_history']: logging_enabled = False - logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details['username']) + logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details[ + 'username']) elif not is_import and not library_details['keep_history']: logging_enabled = False - logger.debug("Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details['section_name']) + logger.debug( + "Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details[ + 'section_name']) if logging_enabled: # Fetch metadata first so we can return false if it fails if not is_import: - logger.debug("Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key']) + logger.debug( + "Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key']) pms_connect = pmsconnect.PmsConnect() if session['live']: metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']), @@ -383,7 +378,7 @@ class ActivityProcessor(object): args = [new_session['id'], new_session['id']] self.db.action(query=query, args=args) - + # logger.debug("Tautulli ActivityProcessor :: Successfully written history item, last id for session_history is %s" # % last_id) diff --git a/jellypy/api2.py b/jellypy/api2.py index 0b14c0cf..9805b1f2 100644 --- a/jellypy/api2.py +++ b/jellypy/api2.py @@ -15,14 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . - -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import object - -from hashing_passwords import check_hash -from io import open - import hashlib import inspect import json @@ -31,39 +23,26 @@ import random import re import time import traceback +from io import open import cherrypy import xmltodict +from hashing_passwords import check_hash import jellypy -if jellypy.PYTHON2: - import common - import config - import database - import helpers - import libraries - import logger - import mobile_app - import notification_handler - import notifiers - import newsletter_handler - import newsletters - import plextv - import users -else: - from jellypy import common - from jellypy import config - from jellypy import database - from jellypy import helpers - from jellypy import libraries - from jellypy import logger - from jellypy import mobile_app - from jellypy import notification_handler - from jellypy import notifiers - from jellypy import newsletter_handler - from jellypy import newsletters - from jellypy import plextv - from jellypy import users +from jellypy import common +from jellypy import config +from jellypy import database +from jellypy import helpers +from jellypy import libraries +from jellypy import logger +from jellypy import mobile_app +from jellypy import notification_handler +from jellypy import notifiers +from jellypy import newsletter_handler +from jellypy import newsletters +from jellypy import plextv +from jellypy import users class API2(object): @@ -91,7 +70,8 @@ class API2(object): if md is True: docs[f] = inspect.getdoc(getattr(self, f)) if inspect.getdoc(getattr(self, f)) else None else: - docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(getattr(self, f)) else None + docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc( + getattr(self, f)) else None return docs def docs_md(self): @@ -128,7 +108,8 @@ class API2(object): self._api_response_code = 400 elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods: - self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods))) + self._api_msg = 'Unknown command: %s. Possible commands are: %s' % ( + kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods))) self._api_response_code = 400 self._api_callback = kwargs.pop('callback', None) @@ -232,7 +213,6 @@ class API2(object): continue if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line: - d = { 'time': temp_loglevel_and_time[0], 'loglevel': loglvl, @@ -778,7 +758,8 @@ General optional parameters: result = call(**self._api_kwargs) except Exception as e: - logger.api_error('Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e)) + logger.api_error( + 'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e)) self._api_response_code = 500 if self._api_debug: cherrypy.request.show_tracebacks = True diff --git a/jellypy/classes.py b/jellypy/classes.py index cde85ab9..9d2a2819 100644 --- a/jellypy/classes.py +++ b/jellypy/classes.py @@ -19,16 +19,7 @@ ## Stolen from Sick-Beard's classes.py ## ######################################### - -from __future__ import unicode_literals - -from future.moves.urllib.request import FancyURLopener - -import jellypy -if jellypy.PYTHON2: - from common import USER_AGENT -else: - from jellypy.common import USER_AGENT +from jellypy.common import USER_AGENT class PlexPyURLopener(FancyURLopener): diff --git a/jellypy/common.py b/jellypy/common.py index c62c2160..ba2c7d14 100644 --- a/jellypy/common.py +++ b/jellypy/common.py @@ -15,21 +15,16 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -import distro import platform from collections import OrderedDict -import jellypy -if jellypy.PYTHON2: - import version -else: - from jellypy import version +import distro +from jellypy import version # Identify Our Application -PRODUCT = 'Tautulli' +PRODUCT = 'JellPy' PLATFORM = platform.system() PLATFORM_RELEASE = platform.release() PLATFORM_VERSION = platform.version() @@ -243,7 +238,7 @@ DATE_TIME_FORMATS = [ {'value': 'YYYY', 'description': 'Numeric, four digits', 'example': '1999, 2003'}, {'value': 'YY', 'description': 'Numeric, two digits', 'example': '99, 03'} ] - }, + }, { 'category': 'Month', 'parameters': [ @@ -253,7 +248,7 @@ DATE_TIME_FORMATS = [ {'value': 'M', 'description': 'Numeric, without leading zeros', 'example': '1-12'}, {'value': 'Mo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 12th'}, ] - }, + }, { 'category': 'Day of the Year', 'parameters': [ @@ -261,7 +256,7 @@ DATE_TIME_FORMATS = [ {'value': 'DDD', 'description': 'Numeric, without leading zeros', 'example': '1-365'}, {'value': 'DDDo', 'description': 'Numeric, with suffix', 'example': '1st, 2nd, ... 365th'}, ] - }, + }, { 'category': 'Day of the Month', 'parameters': [ @@ -269,7 +264,7 @@ DATE_TIME_FORMATS = [ {'value': 'D', 'description': 'Numeric, without leading zeros', 'example': '1-31'}, {'value': 'Do', 'description': 'Numeric, with suffix', 'example': '1st, 2nd ... 31st'}, ] - }, + }, { 'category': 'Day of the Week', 'parameters': [ @@ -279,7 +274,7 @@ DATE_TIME_FORMATS = [ {'value': 'd', 'description': 'Numeric', 'example': '0-6'}, {'value': 'do', 'description': 'Numeric, with suffix', 'example': '0th, 1st ... 6th'}, ] - }, + }, { 'category': 'Hour', 'parameters': [ @@ -288,361 +283,620 @@ DATE_TIME_FORMATS = [ {'value': 'hh', 'description': '12-hour, with leading zeros', 'example': '01-12'}, {'value': 'h', 'description': '12-hour, without leading zeros', 'example': '1-12'}, ] - }, + }, { 'category': 'Minute', 'parameters': [ {'value': 'mm', 'description': 'Numeric, with leading zeros', 'example': '00-59'}, {'value': 'm', 'description': 'Numeric, without leading zeros', 'example': '0-59'}, ] - }, + }, { 'category': 'Second', 'parameters': [ {'value': 'ss', 'description': 'Numeric, with leading zeros', 'example': '00-59'}, {'value': 's', 'description': 'Numeric, without leading zeros', 'example': '0-59'}, ] - }, + }, { 'category': 'AM / PM', 'parameters': [ {'value': 'A', 'description': 'AM/PM uppercase', 'example': 'AM, PM'}, {'value': 'a', 'description': 'am/pm lowercase', 'example': 'am, pm'}, ] - }, + }, { 'category': 'Timezone', 'parameters': [ {'value': 'ZZ', 'description': 'UTC offset', 'example': '+0100, -0700'}, {'value': 'Z', 'description': 'UTC offset', 'example': '+01:00, -07:00'}, ] - }, + }, { 'category': 'Timestamp', 'parameters': [ {'value': 'X', 'description': 'Unix timestamp', 'example': 'E.g. 1456887825'}, ] - }, + }, ] NOTIFICATION_PARAMETERS = [ { 'category': 'Global', 'parameters': [ - {'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', 'description': 'The current version of Tautulli.'}, - {'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', 'description': 'The current git remote of Tautulli.'}, - {'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', 'description': 'The current git branch of Tautulli.'}, - {'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', 'description': 'The current git commit hash of Tautulli.'}, - {'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'}, - {'name': 'Server IP', 'type': 'str', 'value': 'server_ip', 'description': 'The connection IP address for your Plex Server.'}, - {'name': 'Server Port', 'type': 'int', 'value': 'server_port', 'description': 'The connection port for your Plex Server.'}, - {'name': 'Server URL', 'type': 'str', 'value': 'server_url', 'description': 'The connection URL for your Plex Server.'}, - {'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', 'description': 'The platform of your Plex Server.'}, - {'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'}, - {'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'}, - {'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'}, - {'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year when the notification is triggered.'}, - {'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month when the notification is triggered.', 'example': '1 to 12'}, - {'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day when the notification is triggered.', 'example': '1 to 31'}, - {'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour when the notification is triggered.', 'example': '0 to 23'}, - {'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute when the notification is triggered.', 'example': '0 to 59'}, - {'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second when the notification is triggered.', 'example': '0 to 59'}, - {'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday when the notification is triggered.', 'example': '1 (Mon) to 7 (Sun)'}, - {'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number when the notification is triggered.', 'example': '1 to 52'}, - {'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'}, - {'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'}, - {'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'}, - {'name': 'UTC Time', 'type': 'int', 'value': 'utctime', 'description': 'The UTC timestamp in ISO format when the notification is triggered.'}, + {'name': 'Tautulli Version', 'type': 'str', 'value': 'tautulli_version', + 'description': 'The current version of Tautulli.'}, + {'name': 'Tautulli Remote', 'type': 'str', 'value': 'tautulli_remote', + 'description': 'The current git remote of Tautulli.'}, + {'name': 'Tautulli Branch', 'type': 'str', 'value': 'tautulli_branch', + 'description': 'The current git branch of Tautulli.'}, + {'name': 'Tautulli Commit', 'type': 'str', 'value': 'tautulli_commit', + 'description': 'The current git commit hash of Tautulli.'}, + {'name': 'Server Name', 'type': 'str', 'value': 'server_name', + 'description': 'The name of your Plex Server.'}, + {'name': 'Server IP', 'type': 'str', 'value': 'server_ip', + 'description': 'The connection IP address for your Plex Server.'}, + {'name': 'Server Port', 'type': 'int', 'value': 'server_port', + 'description': 'The connection port for your Plex Server.'}, + {'name': 'Server URL', 'type': 'str', 'value': 'server_url', + 'description': 'The connection URL for your Plex Server.'}, + {'name': 'Server Platform', 'type': 'str', 'value': 'server_platform', + 'description': 'The platform of your Plex Server.'}, + {'name': 'Server Version', 'type': 'str', 'value': 'server_version', + 'description': 'The current version of your Plex Server.'}, + {'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', + 'description': 'The unique identifier for your Plex Server.'}, + {'name': 'Action', 'type': 'str', 'value': 'action', + 'description': 'The action that triggered the notification.'}, + {'name': 'Current Year', 'type': 'int', 'value': 'current_year', + 'description': 'The year when the notification is triggered.'}, + {'name': 'Current Month', 'type': 'int', 'value': 'current_month', + 'description': 'The month when the notification is triggered.', 'example': '1 to 12'}, + {'name': 'Current Day', 'type': 'int', 'value': 'current_day', + 'description': 'The day when the notification is triggered.', 'example': '1 to 31'}, + {'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', + 'description': 'The hour when the notification is triggered.', 'example': '0 to 23'}, + {'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', + 'description': 'The minute when the notification is triggered.', 'example': '0 to 59'}, + {'name': 'Current Second', 'type': 'int', 'value': 'current_second', + 'description': 'The second when the notification is triggered.', 'example': '0 to 59'}, + {'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', + 'description': 'The ISO weekday when the notification is triggered.', 'example': '1 (Mon) to 7 (Sun)'}, + {'name': 'Current Week', 'type': 'int', 'value': 'current_week', + 'description': 'The ISO week number when the notification is triggered.', 'example': '1 to 52'}, + {'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', + 'description': 'The date (in date format) when the notification is triggered.'}, + {'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', + 'description': 'The time (in time format) when the notification is triggered.'}, + {'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', + 'description': 'The unix timestamp when the notification is triggered.'}, + {'name': 'UTC Time', 'type': 'int', 'value': 'utctime', + 'description': 'The UTC timestamp in ISO format when the notification is triggered.'}, ] - }, + }, { 'category': 'Stream Details', 'parameters': [ - {'name': 'Streams', 'type': 'int', 'value': 'streams', 'description': 'The total number of concurrent streams.'}, - {'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays', 'description': 'The total number of concurrent direct plays.'}, - {'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams', 'description': 'The total number of concurrent direct streams.'}, - {'name': 'Transcodes', 'type': 'int', 'value': 'transcodes', 'description': 'The total number of concurrent transcodes.'}, - {'name': 'Total Bandwidth', 'type': 'int', 'value': 'total_bandwidth', 'description': 'The total Plex Streaming Brain reserved bandwidth (in kbps).', 'help_text': 'not the used bandwidth'}, - {'name': 'LAN Bandwidth', 'type': 'int', 'value': 'lan_bandwidth', 'description': 'The total Plex Streaming Brain reserved LAN bandwidth (in kbps).', 'help_text': 'not the used bandwidth'}, - {'name': 'WAN Bandwidth', 'type': 'int', 'value': 'wan_bandwidth', 'description': 'The total Plex Streaming Brain reserved WAN bandwidth (in kbps).', 'help_text': 'not the used bandwidth'}, - {'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the user streaming.'}, - {'name': 'User Direct Plays', 'type': 'int', 'value': 'user_direct_plays', 'description': 'The number of concurrent direct plays by the user streaming.'}, - {'name': 'User Direct Streams', 'type': 'int', 'value': 'user_direct_streams', 'description': 'The number of concurrent direct streams by the user streaming.'}, - {'name': 'User Transcodes', 'type': 'int', 'value': 'user_transcodes', 'description': 'The number of concurrent transcodes by the user streaming.'}, - {'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the user streaming.'}, - {'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the user streaming.'}, - {'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the user streaming.'}, - {'name': 'User Thumb', 'type': 'str', 'value': 'user_thumb', 'description': 'The profile picture URL of the user streaming.'}, - {'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'}, - {'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'}, - {'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'}, - {'name': 'Player', 'type': 'str', 'value': 'player', 'description': 'The name of the player being used for playback.'}, - {'name': 'Initial Stream', 'type': 'int', 'value': 'initial_stream', 'description': 'If the stream is the initial stream of a continuous streaming session.', 'example': '0 or 1'}, - {'name': 'IP Address', 'type': 'str', 'value': 'ip_address', 'description': 'The IP address of the device being used for playback.'}, - {'name': 'Stream Duration', 'type': 'int', 'value': 'stream_duration', 'description': 'The duration (in minutes) for the stream.'}, - {'name': 'Stream Time', 'type': 'str', 'value': 'stream_time', 'description': 'The duration (in time format) of the stream.'}, - {'name': 'Remaining Duration', 'type': 'int', 'value': 'remaining_duration', 'description': 'The remaining duration (in minutes) of the stream.'}, - {'name': 'Remaining Time', 'type': 'str', 'value': 'remaining_time', 'description': 'The remaining duration (in time format) of the stream.'}, - {'name': 'Progress Duration', 'type': 'int', 'value': 'progress_duration', 'description': 'The last reported offset (in minutes) of the stream.'}, - {'name': 'Progress Time', 'type': 'str', 'value': 'progress_time', 'description': 'The last reported offset (in time format) of the stream.'}, - {'name': 'Progress Percent', 'type': 'int', 'value': 'progress_percent', 'description': 'The last reported progress percent of the stream.'}, - {'name': 'Transcode Decision', 'type': 'str', 'value': 'transcode_decision', 'description': 'The transcode decision of the stream.'}, - {'name': 'Container Decision', 'type': 'str', 'value': 'container_decision', 'description': 'The container transcode decision of the stream.'}, - {'name': 'Video Decision', 'type': 'str', 'value': 'video_decision', 'description': 'The video transcode decision of the stream.'}, - {'name': 'Audio Decision', 'type': 'str', 'value': 'audio_decision', 'description': 'The audio transcode decision of the stream.'}, - {'name': 'Subtitle Decision', 'type': 'str', 'value': 'subtitle_decision', 'description': 'The subtitle transcode decision of the stream.'}, - {'name': 'Quality Profile', 'type': 'str', 'value': 'quality_profile', 'description': 'The Plex quality profile of the stream.', 'example': 'e.g. Original, 4 Mbps 720p, etc.'}, - {'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version', 'description': 'If the stream is an optimized version.', 'example': '0 or 1'}, - {'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', 'description': 'The optimized version profile of the stream.'}, - {'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', 'description': 'If the stream is an synced version.', 'example': '0 or 1'}, - {'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.', 'example': '0 or 1'}, - {'name': 'Channel Call Sign', 'type': 'str', 'value': 'channel_call_sign', 'description': 'The Live TV channel call sign.'}, - {'name': 'Channel Identifier', 'type': 'str', 'value': 'channel_identifier', 'description': 'The Live TV channel number.'}, - {'name': 'Channel Thumb', 'type': 'str', 'value': 'channel_thumb', 'description': 'The URL for the Live TV channel logo.'}, - {'name': 'Secure', 'type': 'int', 'value': 'secure', 'description': 'If the stream is using a secure connection.', 'example': '0 or 1'}, - {'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.', 'example': '0 or 1'}, - {'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', 'example': '0 or 1'}, - {'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', 'description': 'The network location of the stream.', 'example': 'lan or wan'}, - {'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', 'description': 'The Plex Streaming Brain reserved bandwidth (in kbps) of the stream.', 'help_text': 'not the used bandwidth'}, - {'name': 'Stream Container', 'type': 'str', 'value': 'stream_container', 'description': 'The media container of the stream.'}, - {'name': 'Stream Bitrate', 'type': 'int', 'value': 'stream_bitrate', 'description': 'The bitrate (in kbps) of the stream.'}, - {'name': 'Stream Aspect Ratio', 'type': 'float', 'value': 'stream_aspect_ratio', 'description': 'The aspect ratio of the stream.'}, - {'name': 'Stream Video Codec', 'type': 'str', 'value': 'stream_video_codec', 'description': 'The video codec of the stream.'}, - {'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', 'description': 'The video codec level of the stream.'}, - {'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'}, - {'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'}, - {'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling', 'description': 'The video chroma subsampling of the stream.'}, - {'name': 'Stream Video Color Primaries', 'type': 'str', 'value': 'stream_video_color_primaries', 'description': 'The video color primaries of the stream.'}, - {'name': 'Stream Video Color Range', 'type': 'str', 'value': 'stream_video_color_range', 'description': 'The video color range of the stream.'}, - {'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space', 'description': 'The video color space of the stream.'}, - {'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc', 'description': 'The video transfer function of the stream.'}, - {'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range', 'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'}, - {'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'}, - {'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'}, - {'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'}, - {'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution', 'description': 'The video resolution of the stream.'}, - {'name': 'Stream Video Scan Type', 'type': 'str', 'value': 'stream_video_scan_type', 'description': 'The video scan type of the stream.'}, - {'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height', 'description': 'The video height of the stream.'}, - {'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width', 'description': 'The video width of the stream.'}, - {'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language', 'description': 'The video language of the stream.'}, - {'name': 'Stream Video Language Code', 'type': 'str', 'value': 'stream_video_language_code', 'description': 'The video language code of the stream.'}, - {'name': 'Stream Audio Bitrate', 'type': 'int', 'value': 'stream_audio_bitrate', 'description': 'The audio bitrate of the stream.'}, - {'name': 'Stream Audio Bitrate Mode', 'type': 'str', 'value': 'stream_audio_bitrate_mode', 'description': 'The audio bitrate mode of the stream.', 'example': 'cbr or vbr'}, - {'name': 'Stream Audio Codec', 'type': 'str', 'value': 'stream_audio_codec', 'description': 'The audio codec of the stream.'}, - {'name': 'Stream Audio Channels', 'type': 'float', 'value': 'stream_audio_channels', 'description': 'The audio channels of the stream.'}, - {'name': 'Stream Audio Channel Layout', 'type': 'str', 'value': 'stream_audio_channel_layout', 'description': 'The audio channel layout of the stream.'}, - {'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the stream.'}, - {'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language', 'description': 'The audio language of the stream.'}, - {'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code', 'description': 'The audio language code of the stream.'}, - {'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec', 'description': 'The subtitle codec of the stream.'}, - {'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container', 'description': 'The subtitle container of the stream.'}, - {'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format', 'description': 'The subtitle format of the stream.'}, - {'name': 'Stream Subtitle Forced', 'type': 'int', 'value': 'stream_subtitle_forced', 'description': 'If the subtitles are forced.', 'example': '0 or 1'}, - {'name': 'Stream Subtitle Language', 'type': 'str', 'value': 'stream_subtitle_language', 'description': 'The subtitle language of the stream.'}, - {'name': 'Stream Subtitle Language Code', 'type': 'str', 'value': 'stream_subtitle_language_code', 'description': 'The subtitle language code of the stream.'}, - {'name': 'Stream Subtitle Location', 'type': 'str', 'value': 'stream_subtitle_location', 'description': 'The subtitle location of the stream.'}, - {'name': 'Transcode Container', 'type': 'str', 'value': 'transcode_container', 'description': 'The media container of the transcoded stream.'}, - {'name': 'Transcode Video Codec', 'type': 'str', 'value': 'transcode_video_codec', 'description': 'The video codec of the transcoded stream.'}, - {'name': 'Transcode Video Width', 'type': 'int', 'value': 'transcode_video_width', 'description': 'The video width of the transcoded stream.'}, - {'name': 'Transcode Video Height', 'type': 'int', 'value': 'transcode_video_height', 'description': 'The video height of the transcoded stream.'}, - {'name': 'Transcode Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec', 'description': 'The audio codec of the transcoded stream.'}, - {'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels', 'description': 'The audio channels of the transcoded stream.'}, - {'name': 'Transcode HW Requested', 'type': 'int', 'value': 'transcode_hw_requested', 'description': 'If hardware decoding/encoding was requested.', 'example': '0 or 1'}, - {'name': 'Transcode HW Decoding', 'type': 'int', 'value': 'transcode_hw_decoding', 'description': 'If hardware decoding is used.', 'example': '0 or 1'}, - {'name': 'Transcode HW Decoding Codec', 'type': 'str', 'value': 'transcode_hw_decode', 'description': 'The hardware decoding codec.'}, - {'name': 'Transcode HW Decoding Title', 'type': 'str', 'value': 'transcode_hw_decode_title', 'description': 'The hardware decoding codec title.'}, - {'name': 'Transcode HW Encoding', 'type': 'int', 'value': 'transcode_hw_encoding', 'description': 'If hardware encoding is used.', 'example': '0 or 1'}, - {'name': 'Transcode HW Encoding Codec', 'type': 'str', 'value': 'transcode_hw_encode', 'description': 'The hardware encoding codec.'}, - {'name': 'Transcode HW Encoding Title', 'type': 'str', 'value': 'transcode_hw_encode_title', 'description': 'The hardware encoding codec title.'}, - {'name': 'Session Key', 'type': 'str', 'value': 'session_key', 'description': 'The unique identifier for the session.'}, - {'name': 'Transcode Key', 'type': 'str', 'value': 'transcode_key', 'description': 'The unique identifier for the transcode session.'}, - {'name': 'Session ID', 'type': 'str', 'value': 'session_id', 'description': 'The unique identifier for the stream.'}, - {'name': 'User ID', 'type': 'int', 'value': 'user_id', 'description': 'The unique identifier for the user.'}, - {'name': 'Machine ID', 'type': 'str', 'value': 'machine_id', 'description': 'The unique identifier for the player.'}, - ] - }, + {'name': 'Streams', 'type': 'int', 'value': 'streams', + 'description': 'The total number of concurrent streams.'}, + {'name': 'Direct Plays', 'type': 'int', 'value': 'direct_plays', + 'description': 'The total number of concurrent direct plays.'}, + {'name': 'Direct Streams', 'type': 'int', 'value': 'direct_streams', + 'description': 'The total number of concurrent direct streams.'}, + {'name': 'Transcodes', 'type': 'int', 'value': 'transcodes', + 'description': 'The total number of concurrent transcodes.'}, + {'name': 'Total Bandwidth', 'type': 'int', 'value': 'total_bandwidth', + 'description': 'The total Plex Streaming Brain reserved bandwidth (in kbps).', + 'help_text': 'not the used bandwidth'}, + {'name': 'LAN Bandwidth', 'type': 'int', 'value': 'lan_bandwidth', + 'description': 'The total Plex Streaming Brain reserved LAN bandwidth (in kbps).', + 'help_text': 'not the used bandwidth'}, + {'name': 'WAN Bandwidth', 'type': 'int', 'value': 'wan_bandwidth', + 'description': 'The total Plex Streaming Brain reserved WAN bandwidth (in kbps).', + 'help_text': 'not the used bandwidth'}, + {'name': 'User Streams', 'type': 'int', 'value': 'user_streams', + 'description': 'The number of concurrent streams by the user streaming.'}, + {'name': 'User Direct Plays', 'type': 'int', 'value': 'user_direct_plays', + 'description': 'The number of concurrent direct plays by the user streaming.'}, + {'name': 'User Direct Streams', 'type': 'int', 'value': 'user_direct_streams', + 'description': 'The number of concurrent direct streams by the user streaming.'}, + {'name': 'User Transcodes', 'type': 'int', 'value': 'user_transcodes', + 'description': 'The number of concurrent transcodes by the user streaming.'}, + {'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the user streaming.'}, + {'name': 'Username', 'type': 'str', 'value': 'username', + 'description': 'The username of the user streaming.'}, + {'name': 'User Email', 'type': 'str', 'value': 'user_email', + 'description': 'The email address of the user streaming.'}, + {'name': 'User Thumb', 'type': 'str', 'value': 'user_thumb', + 'description': 'The profile picture URL of the user streaming.'}, + {'name': 'Device', 'type': 'str', 'value': 'device', + 'description': 'The type of client device being used for playback.'}, + {'name': 'Platform', 'type': 'str', 'value': 'platform', + 'description': 'The type of client platform being used for playback.'}, + {'name': 'Product', 'type': 'str', 'value': 'product', + 'description': 'The type of client product being used for playback.'}, + {'name': 'Player', 'type': 'str', 'value': 'player', + 'description': 'The name of the player being used for playback.'}, + {'name': 'Initial Stream', 'type': 'int', 'value': 'initial_stream', + 'description': 'If the stream is the initial stream of a continuous streaming session.', + 'example': '0 or 1'}, + {'name': 'IP Address', 'type': 'str', 'value': 'ip_address', + 'description': 'The IP address of the device being used for playback.'}, + {'name': 'Stream Duration', 'type': 'int', 'value': 'stream_duration', + 'description': 'The duration (in minutes) for the stream.'}, + {'name': 'Stream Time', 'type': 'str', 'value': 'stream_time', + 'description': 'The duration (in time format) of the stream.'}, + {'name': 'Remaining Duration', 'type': 'int', 'value': 'remaining_duration', + 'description': 'The remaining duration (in minutes) of the stream.'}, + {'name': 'Remaining Time', 'type': 'str', 'value': 'remaining_time', + 'description': 'The remaining duration (in time format) of the stream.'}, + {'name': 'Progress Duration', 'type': 'int', 'value': 'progress_duration', + 'description': 'The last reported offset (in minutes) of the stream.'}, + {'name': 'Progress Time', 'type': 'str', 'value': 'progress_time', + 'description': 'The last reported offset (in time format) of the stream.'}, + {'name': 'Progress Percent', 'type': 'int', 'value': 'progress_percent', + 'description': 'The last reported progress percent of the stream.'}, + {'name': 'Transcode Decision', 'type': 'str', 'value': 'transcode_decision', + 'description': 'The transcode decision of the stream.'}, + {'name': 'Container Decision', 'type': 'str', 'value': 'container_decision', + 'description': 'The container transcode decision of the stream.'}, + {'name': 'Video Decision', 'type': 'str', 'value': 'video_decision', + 'description': 'The video transcode decision of the stream.'}, + {'name': 'Audio Decision', 'type': 'str', 'value': 'audio_decision', + 'description': 'The audio transcode decision of the stream.'}, + {'name': 'Subtitle Decision', 'type': 'str', 'value': 'subtitle_decision', + 'description': 'The subtitle transcode decision of the stream.'}, + {'name': 'Quality Profile', 'type': 'str', 'value': 'quality_profile', + 'description': 'The Plex quality profile of the stream.', 'example': 'e.g. Original, 4 Mbps 720p, etc.'}, + {'name': 'Optimized Version', 'type': 'int', 'value': 'optimized_version', + 'description': 'If the stream is an optimized version.', 'example': '0 or 1'}, + {'name': 'Optimized Version Profile', 'type': 'str', 'value': 'optimized_version_profile', + 'description': 'The optimized version profile of the stream.'}, + {'name': 'Synced Version', 'type': 'int', 'value': 'synced_version', + 'description': 'If the stream is an synced version.', 'example': '0 or 1'}, + {'name': 'Live', 'type': 'int', 'value': 'live', 'description': 'If the stream is live TV.', + 'example': '0 or 1'}, + {'name': 'Channel Call Sign', 'type': 'str', 'value': 'channel_call_sign', + 'description': 'The Live TV channel call sign.'}, + {'name': 'Channel Identifier', 'type': 'str', 'value': 'channel_identifier', + 'description': 'The Live TV channel number.'}, + {'name': 'Channel Thumb', 'type': 'str', 'value': 'channel_thumb', + 'description': 'The URL for the Live TV channel logo.'}, + {'name': 'Secure', 'type': 'int', 'value': 'secure', + 'description': 'If the stream is using a secure connection.', 'example': '0 or 1'}, + {'name': 'Relayed', 'type': 'int', 'value': 'relayed', 'description': 'If the stream is using Plex Relay.', + 'example': '0 or 1'}, + {'name': 'Stream Local', 'type': 'int', 'value': 'stream_local', 'description': 'If the stream is local.', + 'example': '0 or 1'}, + {'name': 'Stream Location', 'type': 'str', 'value': 'stream_location', + 'description': 'The network location of the stream.', 'example': 'lan or wan'}, + {'name': 'Stream Bandwidth', 'type': 'int', 'value': 'stream_bandwidth', + 'description': 'The Plex Streaming Brain reserved bandwidth (in kbps) of the stream.', + 'help_text': 'not the used bandwidth'}, + {'name': 'Stream Container', 'type': 'str', 'value': 'stream_container', + 'description': 'The media container of the stream.'}, + {'name': 'Stream Bitrate', 'type': 'int', 'value': 'stream_bitrate', + 'description': 'The bitrate (in kbps) of the stream.'}, + {'name': 'Stream Aspect Ratio', 'type': 'float', 'value': 'stream_aspect_ratio', + 'description': 'The aspect ratio of the stream.'}, + {'name': 'Stream Video Codec', 'type': 'str', 'value': 'stream_video_codec', + 'description': 'The video codec of the stream.'}, + {'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', + 'description': 'The video codec level of the stream.'}, + {'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', + 'description': 'The video bitrate (in kbps) of the stream.'}, + {'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', + 'description': 'The video bit depth of the stream.'}, + {'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling', + 'description': 'The video chroma subsampling of the stream.'}, + {'name': 'Stream Video Color Primaries', 'type': 'str', 'value': 'stream_video_color_primaries', + 'description': 'The video color primaries of the stream.'}, + {'name': 'Stream Video Color Range', 'type': 'str', 'value': 'stream_video_color_range', + 'description': 'The video color range of the stream.'}, + {'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space', + 'description': 'The video color space of the stream.'}, + {'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc', + 'description': 'The video transfer function of the stream.'}, + {'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range', + 'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'}, + {'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', + 'description': 'The video framerate of the stream.'}, + {'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', + 'description': 'The video resolution of the stream with scan type.'}, + {'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', + 'description': 'The video reference frames of the stream.'}, + {'name': 'Stream Video Resolution', 'type': 'str', 'value': 'stream_video_resolution', + 'description': 'The video resolution of the stream.'}, + {'name': 'Stream Video Scan Type', 'type': 'str', 'value': 'stream_video_scan_type', + 'description': 'The video scan type of the stream.'}, + {'name': 'Stream Video Height', 'type': 'int', 'value': 'stream_video_height', + 'description': 'The video height of the stream.'}, + {'name': 'Stream Video Width', 'type': 'int', 'value': 'stream_video_width', + 'description': 'The video width of the stream.'}, + {'name': 'Stream Video Language', 'type': 'str', 'value': 'stream_video_language', + 'description': 'The video language of the stream.'}, + {'name': 'Stream Video Language Code', 'type': 'str', 'value': 'stream_video_language_code', + 'description': 'The video language code of the stream.'}, + {'name': 'Stream Audio Bitrate', 'type': 'int', 'value': 'stream_audio_bitrate', + 'description': 'The audio bitrate of the stream.'}, + {'name': 'Stream Audio Bitrate Mode', 'type': 'str', 'value': 'stream_audio_bitrate_mode', + 'description': 'The audio bitrate mode of the stream.', 'example': 'cbr or vbr'}, + {'name': 'Stream Audio Codec', 'type': 'str', 'value': 'stream_audio_codec', + 'description': 'The audio codec of the stream.'}, + {'name': 'Stream Audio Channels', 'type': 'float', 'value': 'stream_audio_channels', + 'description': 'The audio channels of the stream.'}, + {'name': 'Stream Audio Channel Layout', 'type': 'str', 'value': 'stream_audio_channel_layout', + 'description': 'The audio channel layout of the stream.'}, + {'name': 'Stream Audio Sample Rate', 'type': 'int', 'value': 'stream_audio_sample_rate', + 'description': 'The audio sample rate (in Hz) of the stream.'}, + {'name': 'Stream Audio Language', 'type': 'str', 'value': 'stream_audio_language', + 'description': 'The audio language of the stream.'}, + {'name': 'Stream Audio Language Code', 'type': 'str', 'value': 'stream_audio_language_code', + 'description': 'The audio language code of the stream.'}, + {'name': 'Stream Subtitle Codec', 'type': 'str', 'value': 'stream_subtitle_codec', + 'description': 'The subtitle codec of the stream.'}, + {'name': 'Stream Subtitle Container', 'type': 'str', 'value': 'stream_subtitle_container', + 'description': 'The subtitle container of the stream.'}, + {'name': 'Stream Subtitle Format', 'type': 'str', 'value': 'stream_subtitle_format', + 'description': 'The subtitle format of the stream.'}, + {'name': 'Stream Subtitle Forced', 'type': 'int', 'value': 'stream_subtitle_forced', + 'description': 'If the subtitles are forced.', 'example': '0 or 1'}, + {'name': 'Stream Subtitle Language', 'type': 'str', 'value': 'stream_subtitle_language', + 'description': 'The subtitle language of the stream.'}, + {'name': 'Stream Subtitle Language Code', 'type': 'str', 'value': 'stream_subtitle_language_code', + 'description': 'The subtitle language code of the stream.'}, + {'name': 'Stream Subtitle Location', 'type': 'str', 'value': 'stream_subtitle_location', + 'description': 'The subtitle location of the stream.'}, + {'name': 'Transcode Container', 'type': 'str', 'value': 'transcode_container', + 'description': 'The media container of the transcoded stream.'}, + {'name': 'Transcode Video Codec', 'type': 'str', 'value': 'transcode_video_codec', + 'description': 'The video codec of the transcoded stream.'}, + {'name': 'Transcode Video Width', 'type': 'int', 'value': 'transcode_video_width', + 'description': 'The video width of the transcoded stream.'}, + {'name': 'Transcode Video Height', 'type': 'int', 'value': 'transcode_video_height', + 'description': 'The video height of the transcoded stream.'}, + {'name': 'Transcode Audio Codec', 'type': 'str', 'value': 'transcode_audio_codec', + 'description': 'The audio codec of the transcoded stream.'}, + {'name': 'Transcode Audio Channels', 'type': 'float', 'value': 'transcode_audio_channels', + 'description': 'The audio channels of the transcoded stream.'}, + {'name': 'Transcode HW Requested', 'type': 'int', 'value': 'transcode_hw_requested', + 'description': 'If hardware decoding/encoding was requested.', 'example': '0 or 1'}, + {'name': 'Transcode HW Decoding', 'type': 'int', 'value': 'transcode_hw_decoding', + 'description': 'If hardware decoding is used.', 'example': '0 or 1'}, + {'name': 'Transcode HW Decoding Codec', 'type': 'str', 'value': 'transcode_hw_decode', + 'description': 'The hardware decoding codec.'}, + {'name': 'Transcode HW Decoding Title', 'type': 'str', 'value': 'transcode_hw_decode_title', + 'description': 'The hardware decoding codec title.'}, + {'name': 'Transcode HW Encoding', 'type': 'int', 'value': 'transcode_hw_encoding', + 'description': 'If hardware encoding is used.', 'example': '0 or 1'}, + {'name': 'Transcode HW Encoding Codec', 'type': 'str', 'value': 'transcode_hw_encode', + 'description': 'The hardware encoding codec.'}, + {'name': 'Transcode HW Encoding Title', 'type': 'str', 'value': 'transcode_hw_encode_title', + 'description': 'The hardware encoding codec title.'}, + {'name': 'Session Key', 'type': 'str', 'value': 'session_key', + 'description': 'The unique identifier for the session.'}, + {'name': 'Transcode Key', 'type': 'str', 'value': 'transcode_key', + 'description': 'The unique identifier for the transcode session.'}, + {'name': 'Session ID', 'type': 'str', 'value': 'session_id', + 'description': 'The unique identifier for the stream.'}, + {'name': 'User ID', 'type': 'int', 'value': 'user_id', + 'description': 'The unique identifier for the user.'}, + {'name': 'Machine ID', 'type': 'str', 'value': 'machine_id', + 'description': 'The unique identifier for the player.'}, + ] + }, { 'category': 'Source Metadata Details', 'parameters': [ - {'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', 'example': 'movie, show, season, episode, artist, album, track, clip'}, - {'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'}, - {'name': 'Library Name', 'type': 'str', 'value': 'library_name', 'description': 'The library name of the item.'}, - {'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'}, - {'name': 'Episode Name', 'type': 'str', 'value': 'episode_name', 'description': 'The title of the episode.'}, - {'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'}, - {'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'}, - {'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'}, - {'name': 'Track Artist', 'type': 'str', 'value': 'track_artist', 'description': 'The name of the artist of the track.'}, - {'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.', 'example': 'e.g. 1, or 1-3'}, - {'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00', 'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'}, - {'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', 'example': 'e.g. 6, or 6-10'}, - {'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'}, - {'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'}, - {'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'}, - {'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'}, - {'name': 'Episode Count', 'type': 'int', 'value': 'episode_count', 'description': 'The number of episodes.'}, - {'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'}, - {'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'}, - {'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'}, - {'name': 'Release Date', 'type': 'str', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'}, - {'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'}, - {'name': 'Added Date', 'type': 'str', 'value': 'added_date', 'description': 'The date (in date format) the item was added to Plex.'}, - {'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'}, - {'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'}, - {'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'}, - {'name': 'Content Rating', 'type': 'str', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'}, - {'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'}, - {'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'}, - {'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'}, - {'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'}, - {'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'}, - {'name': 'Collections', 'type': 'str', 'value': 'collections', 'description': 'A list of collections for the item.'}, - {'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'}, - {'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'}, - {'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'}, - {'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating', 'description': 'The critic rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'}, - {'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating', 'description': 'The audience rating for the item.', 'help_text': 'Rating out of 10 for IMDB, percentage (%) for Rotten Tomatoes and TMDB.'}, - {'name': 'User Rating', 'type': 'float', 'value': 'user_rating', 'description': 'The user (star) rating (out of 10) for the item.'}, - {'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'}, - {'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'}, - {'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.', 'example': 'e.g. 5d7769a9594b2b001e6a6b7e'}, - {'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'}, - {'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.', 'example': 'e.g. tt2488496'}, - {'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'}, - {'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the TV show.', 'example': 'e.g. 121361'}, - {'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the TV show.'}, - {'name': 'TMDB ID', 'type': 'int', 'value': 'themoviedb_id', 'description': 'The TMDb ID for the movie or TV show.', 'example': 'e.g. 15260'}, - {'name': 'TMDB URL', 'type': 'str', 'value': 'themoviedb_url', 'description': 'The TMDb URL for the movie or TV show.'}, - {'name': 'TVmaze ID', 'type': 'int', 'value': 'tvmaze_id', 'description': 'The TVmaze ID for the TV show.', 'example': 'e.g. 290'}, - {'name': 'TVmaze URL', 'type': 'str', 'value': 'tvmaze_url', 'description': 'The TVmaze URL for the TV show.'}, - {'name': 'MusicBrainz ID', 'type': 'str', 'value': 'musicbrainz_id', 'description': 'The MusicBrainz ID for the artist, album, or track.', 'example': 'e.g. b670dfcf-9824-4309-a57e-03595aaba286'}, - {'name': 'MusicBrainz URL', 'type': 'str', 'value': 'musicbrainz_url', 'description': 'The MusicBrainz URL for the artist, album, or track.'}, - {'name': 'Last.fm URL', 'type': 'str', 'value': 'lastfm_url', 'description': 'The Last.fm URL for the album.', 'help_text': 'Music library agent must be Last.fm'}, - {'name': 'Trakt.tv URL', 'type': 'str', 'value': 'trakt_url', 'description': 'The trakt.tv URL for the movie or TV show.'}, - {'name': 'Container', 'type': 'str', 'value': 'container', 'description': 'The media container of the original media.'}, - {'name': 'Bitrate', 'type': 'int', 'value': 'bitrate', 'description': 'The bitrate of the original media.'}, - {'name': 'Aspect Ratio', 'type': 'float', 'value': 'aspect_ratio', 'description': 'The aspect ratio of the original media.'}, - {'name': 'Video Codec', 'type': 'str', 'value': 'video_codec', 'description': 'The video codec of the original media.'}, - {'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', 'description': 'The video codec level of the original media.'}, - {'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'}, - {'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'}, - {'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling', 'description': 'The video chroma subsampling of the original media.'}, - {'name': 'Video Color Primaries', 'type': 'str', 'value': 'video_color_primaries', 'description': 'The video color primaries of the original media.'}, - {'name': 'Video Color Range', 'type': 'str', 'value': 'video_color_range', 'description': 'The video color range of the original media.'}, - {'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space', 'description': 'The video color space of the original media.'}, - {'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc', 'description': 'The video transfer function of the original media.'}, - {'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range', 'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'}, - {'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'}, - {'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'}, - {'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'}, - {'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution', 'description': 'The video resolution of the original media.'}, - {'name': 'Video Scan Type', 'type': 'str', 'value': 'video_scan_type', 'description': 'The video scan type of the original media.'}, - {'name': 'Video Height', 'type': 'int', 'value': 'video_height', 'description': 'The video height of the original media.'}, - {'name': 'Video Width', 'type': 'int', 'value': 'video_width', 'description': 'The video width of the original media.'}, - {'name': 'Video Language', 'type': 'str', 'value': 'video_language', 'description': 'The video language of the original media.'}, - {'name': 'Video Language Code', 'type': 'str', 'value': 'video_language_code', 'description': 'The video language code of the original media.'}, - {'name': 'Audio Bitrate', 'type': 'int', 'value': 'audio_bitrate', 'description': 'The audio bitrate of the original media.'}, - {'name': 'Audio Bitrate Mode', 'type': 'str', 'value': 'audio_bitrate_mode', 'description': 'The audio bitrate mode of the original media.', 'example': 'cbr or vbr'}, - {'name': 'Audio Codec', 'type': 'str', 'value': 'audio_codec', 'description': 'The audio codec of the original media.'}, - {'name': 'Audio Channels', 'type': 'float', 'value': 'audio_channels', 'description': 'The audio channels of the original media.'}, - {'name': 'Audio Channel Layout', 'type': 'str', 'value': 'audio_channel_layout', 'description': 'The audio channel layout of the original media.'}, - {'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate', 'description': 'The audio sample rate (in Hz) of the original media.'}, - {'name': 'Audio Language', 'type': 'str', 'value': 'audio_language', 'description': 'The audio language of the original media.'}, - {'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code', 'description': 'The audio language code of the original media.'}, - {'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec', 'description': 'The subtitle codec of the original media.'}, - {'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container', 'description': 'The subtitle container of the original media.'}, - {'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format', 'description': 'The subtitle format of the original media.'}, - {'name': 'Subtitle Forced', 'type': 'int', 'value': 'subtitle_forced', 'description': 'If the subtitles are forced.', 'example': '0 or 1'}, - {'name': 'Subtitle Location', 'type': 'str', 'value': 'subtitle_location', 'description': 'The subtitle location of the original media.'}, - {'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'}, - {'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'}, - {'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'}, - {'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'}, - {'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'}, - {'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'}, - {'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'}, - {'name': 'Parent Rating Key', 'type': 'int', 'value': 'parent_rating_key', 'description': 'The unique identifier for the season or album.'}, - {'name': 'Grandparent Rating Key', 'type': 'int', 'value': 'grandparent_rating_key', 'description': 'The unique identifier for the TV show or artist.'}, - {'name': 'Art', 'type': 'str', 'value': 'art', 'description': 'The Plex background art for the media.'}, - {'name': 'Thumb', 'type': 'str', 'value': 'thumb', 'description': 'The Plex thumbnail for the movie or episode.'}, - {'name': 'Parent Thumb', 'type': 'str', 'value': 'parent_thumb', 'description': 'The Plex thumbnail for the season or album.'}, - {'name': 'Grandparent Thumb', 'type': 'str', 'value': 'grandparent_thumb', 'description': 'The Plex thumbnail for the TV show or artist.'}, - {'name': 'Poster Thumb', 'type': 'str', 'value': 'poster_thumb', 'description': 'The Plex thumbnail for the poster image.'}, - {'name': 'Poster Title', 'type': 'str', 'value': 'poster_title', 'description': 'The title for the poster image.'}, - {'name': 'Indexes', 'type': 'int', 'value': 'indexes', 'description': 'If the media has video preview thumbnails.', 'example': '0 or 1'}, - ] - }, + {'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', + 'example': 'movie, show, season, episode, artist, album, track, clip'}, + {'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'}, + {'name': 'Library Name', 'type': 'str', 'value': 'library_name', + 'description': 'The library name of the item.'}, + {'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'}, + {'name': 'Episode Name', 'type': 'str', 'value': 'episode_name', + 'description': 'The title of the episode.'}, + {'name': 'Artist Name', 'type': 'str', 'value': 'artist_name', 'description': 'The name of the artist.'}, + {'name': 'Album Name', 'type': 'str', 'value': 'album_name', 'description': 'The title of the album.'}, + {'name': 'Track Name', 'type': 'str', 'value': 'track_name', 'description': 'The title of the track.'}, + {'name': 'Track Artist', 'type': 'str', 'value': 'track_artist', + 'description': 'The name of the artist of the track.'}, + {'name': 'Season Number', 'type': 'int', 'value': 'season_num', 'description': 'The season number.', + 'example': 'e.g. 1, or 1-3'}, + {'name': 'Season Number 00', 'type': 'int', 'value': 'season_num00', + 'description': 'The two digit season number.', 'example': 'e.g. 01, or 01-03'}, + {'name': 'Episode Number', 'type': 'int', 'value': 'episode_num', 'description': 'The episode number.', + 'example': 'e.g. 6, or 6-10'}, + {'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', + 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'}, + {'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', + 'example': 'e.g. 4, or 4-10'}, + {'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', + 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'}, + {'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'}, + {'name': 'Episode Count', 'type': 'int', 'value': 'episode_count', + 'description': 'The number of episodes.'}, + {'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'}, + {'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'}, + {'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'}, + {'name': 'Release Date', 'type': 'str', 'value': 'release_date', + 'description': 'The release date (in date format) for the item.'}, + {'name': 'Air Date', 'type': 'str', 'value': 'air_date', + 'description': 'The air date (in date format) for the item.'}, + {'name': 'Added Date', 'type': 'str', 'value': 'added_date', + 'description': 'The date (in date format) the item was added to Plex.'}, + {'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', + 'description': 'The date (in date format) the item was updated on Plex.'}, + {'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', + 'description': 'The date (in date format) the item was last viewed on Plex.'}, + {'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'}, + {'name': 'Content Rating', 'type': 'str', 'value': 'content_rating', + 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'}, + {'name': 'Directors', 'type': 'str', 'value': 'directors', + 'description': 'A list of directors for the item.'}, + {'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'}, + {'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'}, + {'name': 'Genres', 'type': 'str', 'value': 'genres', 'description': 'A list of genres for the item.'}, + {'name': 'Labels', 'type': 'str', 'value': 'labels', 'description': 'A list of labels for the item.'}, + {'name': 'Collections', 'type': 'str', 'value': 'collections', + 'description': 'A list of collections for the item.'}, + {'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'}, + {'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'}, + {'name': 'Rating', 'type': 'float', 'value': 'rating', + 'description': 'The rating (out of 10) for the item.'}, + {'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating', + 'description': 'The critic rating (%) for the item.', + 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'}, + {'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating', + 'description': 'The audience rating for the item.', + 'help_text': 'Rating out of 10 for IMDB, percentage (%) for Rotten Tomatoes and TMDB.'}, + {'name': 'User Rating', 'type': 'float', 'value': 'user_rating', + 'description': 'The user (star) rating (out of 10) for the item.'}, + {'name': 'Duration', 'type': 'int', 'value': 'duration', + 'description': 'The duration (in minutes) for the item.'}, + {'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', + 'description': 'A URL for the movie, TV show, or album poster.'}, + {'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.', + 'example': 'e.g. 5d7769a9594b2b001e6a6b7e'}, + {'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', + 'description': 'The Plex URL to your server for the item.'}, + {'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.', + 'example': 'e.g. tt2488496'}, + {'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'}, + {'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the TV show.', + 'example': 'e.g. 121361'}, + {'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the TV show.'}, + {'name': 'TMDB ID', 'type': 'int', 'value': 'themoviedb_id', + 'description': 'The TMDb ID for the movie or TV show.', 'example': 'e.g. 15260'}, + {'name': 'TMDB URL', 'type': 'str', 'value': 'themoviedb_url', + 'description': 'The TMDb URL for the movie or TV show.'}, + {'name': 'TVmaze ID', 'type': 'int', 'value': 'tvmaze_id', 'description': 'The TVmaze ID for the TV show.', + 'example': 'e.g. 290'}, + {'name': 'TVmaze URL', 'type': 'str', 'value': 'tvmaze_url', + 'description': 'The TVmaze URL for the TV show.'}, + {'name': 'MusicBrainz ID', 'type': 'str', 'value': 'musicbrainz_id', + 'description': 'The MusicBrainz ID for the artist, album, or track.', + 'example': 'e.g. b670dfcf-9824-4309-a57e-03595aaba286'}, + {'name': 'MusicBrainz URL', 'type': 'str', 'value': 'musicbrainz_url', + 'description': 'The MusicBrainz URL for the artist, album, or track.'}, + {'name': 'Last.fm URL', 'type': 'str', 'value': 'lastfm_url', + 'description': 'The Last.fm URL for the album.', 'help_text': 'Music library agent must be Last.fm'}, + {'name': 'Trakt.tv URL', 'type': 'str', 'value': 'trakt_url', + 'description': 'The trakt.tv URL for the movie or TV show.'}, + {'name': 'Container', 'type': 'str', 'value': 'container', + 'description': 'The media container of the original media.'}, + {'name': 'Bitrate', 'type': 'int', 'value': 'bitrate', 'description': 'The bitrate of the original media.'}, + {'name': 'Aspect Ratio', 'type': 'float', 'value': 'aspect_ratio', + 'description': 'The aspect ratio of the original media.'}, + {'name': 'Video Codec', 'type': 'str', 'value': 'video_codec', + 'description': 'The video codec of the original media.'}, + {'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', + 'description': 'The video codec level of the original media.'}, + {'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', + 'description': 'The video bitrate of the original media.'}, + {'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', + 'description': 'The video bit depth of the original media.'}, + {'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling', + 'description': 'The video chroma subsampling of the original media.'}, + {'name': 'Video Color Primaries', 'type': 'str', 'value': 'video_color_primaries', + 'description': 'The video color primaries of the original media.'}, + {'name': 'Video Color Range', 'type': 'str', 'value': 'video_color_range', + 'description': 'The video color range of the original media.'}, + {'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space', + 'description': 'The video color space of the original media.'}, + {'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc', + 'description': 'The video transfer function of the original media.'}, + {'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range', + 'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'}, + {'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', + 'description': 'The video framerate of the original media.'}, + {'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', + 'description': 'The video resolution of the original media with scan type.'}, + {'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', + 'description': 'The video reference frames of the original media.'}, + {'name': 'Video Resolution', 'type': 'str', 'value': 'video_resolution', + 'description': 'The video resolution of the original media.'}, + {'name': 'Video Scan Type', 'type': 'str', 'value': 'video_scan_type', + 'description': 'The video scan type of the original media.'}, + {'name': 'Video Height', 'type': 'int', 'value': 'video_height', + 'description': 'The video height of the original media.'}, + {'name': 'Video Width', 'type': 'int', 'value': 'video_width', + 'description': 'The video width of the original media.'}, + {'name': 'Video Language', 'type': 'str', 'value': 'video_language', + 'description': 'The video language of the original media.'}, + {'name': 'Video Language Code', 'type': 'str', 'value': 'video_language_code', + 'description': 'The video language code of the original media.'}, + {'name': 'Audio Bitrate', 'type': 'int', 'value': 'audio_bitrate', + 'description': 'The audio bitrate of the original media.'}, + {'name': 'Audio Bitrate Mode', 'type': 'str', 'value': 'audio_bitrate_mode', + 'description': 'The audio bitrate mode of the original media.', 'example': 'cbr or vbr'}, + {'name': 'Audio Codec', 'type': 'str', 'value': 'audio_codec', + 'description': 'The audio codec of the original media.'}, + {'name': 'Audio Channels', 'type': 'float', 'value': 'audio_channels', + 'description': 'The audio channels of the original media.'}, + {'name': 'Audio Channel Layout', 'type': 'str', 'value': 'audio_channel_layout', + 'description': 'The audio channel layout of the original media.'}, + {'name': 'Audio Sample Rate', 'type': 'int', 'value': 'audio_sample_rate', + 'description': 'The audio sample rate (in Hz) of the original media.'}, + {'name': 'Audio Language', 'type': 'str', 'value': 'audio_language', + 'description': 'The audio language of the original media.'}, + {'name': 'Audio Language Code', 'type': 'str', 'value': 'audio_language_code', + 'description': 'The audio language code of the original media.'}, + {'name': 'Subtitle Codec', 'type': 'str', 'value': 'subtitle_codec', + 'description': 'The subtitle codec of the original media.'}, + {'name': 'Subtitle Container', 'type': 'str', 'value': 'subtitle_container', + 'description': 'The subtitle container of the original media.'}, + {'name': 'Subtitle Format', 'type': 'str', 'value': 'subtitle_format', + 'description': 'The subtitle format of the original media.'}, + {'name': 'Subtitle Forced', 'type': 'int', 'value': 'subtitle_forced', + 'description': 'If the subtitles are forced.', 'example': '0 or 1'}, + {'name': 'Subtitle Location', 'type': 'str', 'value': 'subtitle_location', + 'description': 'The subtitle location of the original media.'}, + {'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', + 'description': 'The subtitle language of the original media.'}, + {'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', + 'description': 'The subtitle language code of the original media.'}, + {'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'}, + {'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'}, + {'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'}, + {'name': 'Section ID', 'type': 'int', 'value': 'section_id', + 'description': 'The unique identifier for the library.'}, + {'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', + 'description': 'The unique identifier for the movie, episode, or track.'}, + {'name': 'Parent Rating Key', 'type': 'int', 'value': 'parent_rating_key', + 'description': 'The unique identifier for the season or album.'}, + {'name': 'Grandparent Rating Key', 'type': 'int', 'value': 'grandparent_rating_key', + 'description': 'The unique identifier for the TV show or artist.'}, + {'name': 'Art', 'type': 'str', 'value': 'art', 'description': 'The Plex background art for the media.'}, + {'name': 'Thumb', 'type': 'str', 'value': 'thumb', + 'description': 'The Plex thumbnail for the movie or episode.'}, + {'name': 'Parent Thumb', 'type': 'str', 'value': 'parent_thumb', + 'description': 'The Plex thumbnail for the season or album.'}, + {'name': 'Grandparent Thumb', 'type': 'str', 'value': 'grandparent_thumb', + 'description': 'The Plex thumbnail for the TV show or artist.'}, + {'name': 'Poster Thumb', 'type': 'str', 'value': 'poster_thumb', + 'description': 'The Plex thumbnail for the poster image.'}, + {'name': 'Poster Title', 'type': 'str', 'value': 'poster_title', + 'description': 'The title for the poster image.'}, + {'name': 'Indexes', 'type': 'int', 'value': 'indexes', + 'description': 'If the media has video preview thumbnails.', 'example': '0 or 1'}, + ] + }, { 'category': 'Plex Remote Access', 'parameters': [ - {'name': 'Remote Access Mapping State', 'type': 'str', 'value': 'remote_access_mapping_state', 'description': 'The mapping state of the Plex remote access port.'}, - {'name': 'Remote Access Mapping Error', 'type': 'str', 'value': 'remote_access_mapping_error', 'description': 'The mapping error of the Plex remote access port.'}, - {'name': 'Remote Access Public IP Address', 'type': 'str', 'value': 'remote_access_public_address', 'description': 'The Plex remote access public IP address.'}, - {'name': 'Remote Access Public Port', 'type': 'str', 'value': 'remote_access_public_port', 'description': 'The Plex remote access public port.'}, - {'name': 'Remote Access Private IP Address', 'type': 'str', 'value': 'remote_access_private_address', 'description': 'The Plex remote access private IP address.'}, - {'name': 'Remote Access Private Port', 'type': 'str', 'value': 'remote_access_private_port', 'description': 'The Plex remote access private port.'}, - {'name': 'Remote Access Failure Reason', 'type': 'str', 'value': 'remote_access_reason', 'description': 'The failure reason for Plex remote access going down.'}, + {'name': 'Remote Access Mapping State', 'type': 'str', 'value': 'remote_access_mapping_state', + 'description': 'The mapping state of the Plex remote access port.'}, + {'name': 'Remote Access Mapping Error', 'type': 'str', 'value': 'remote_access_mapping_error', + 'description': 'The mapping error of the Plex remote access port.'}, + {'name': 'Remote Access Public IP Address', 'type': 'str', 'value': 'remote_access_public_address', + 'description': 'The Plex remote access public IP address.'}, + {'name': 'Remote Access Public Port', 'type': 'str', 'value': 'remote_access_public_port', + 'description': 'The Plex remote access public port.'}, + {'name': 'Remote Access Private IP Address', 'type': 'str', 'value': 'remote_access_private_address', + 'description': 'The Plex remote access private IP address.'}, + {'name': 'Remote Access Private Port', 'type': 'str', 'value': 'remote_access_private_port', + 'description': 'The Plex remote access private port.'}, + {'name': 'Remote Access Failure Reason', 'type': 'str', 'value': 'remote_access_reason', + 'description': 'The failure reason for Plex remote access going down.'}, ] - }, + }, { 'category': 'Plex Update Available', 'parameters': [ - {'name': 'Update Version', 'type': 'str', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'}, - {'name': 'Update Url', 'type': 'str', 'value': 'update_url', 'description': 'The download URL for the available update.'}, - {'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', 'description': 'The release date of the available update.'}, - {'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'}, - {'name': 'Update Platform', 'type': 'str', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'}, - {'name': 'Update Distro', 'type': 'str', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'}, - {'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build', 'description': 'The distro build of your Plex Server.'}, - {'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements', 'description': 'The requirements for the available update.'}, - {'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info', 'description': 'Any extra info for the available update.'}, - {'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added', 'description': 'The added changelog for the available update.'}, - {'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed', 'description': 'The fixed changelog for the available update.'}, - ] - }, + {'name': 'Update Version', 'type': 'str', 'value': 'update_version', + 'description': 'The available update version for your Plex Server.'}, + {'name': 'Update Url', 'type': 'str', 'value': 'update_url', + 'description': 'The download URL for the available update.'}, + {'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', + 'description': 'The release date of the available update.'}, + {'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.', + 'example': 'Public or Plex Pass'}, + {'name': 'Update Platform', 'type': 'str', 'value': 'update_platform', + 'description': 'The platform of your Plex Server.'}, + {'name': 'Update Distro', 'type': 'str', 'value': 'update_distro', + 'description': 'The distro of your Plex Server.'}, + {'name': 'Update Distro Build', 'type': 'str', 'value': 'update_distro_build', + 'description': 'The distro build of your Plex Server.'}, + {'name': 'Update Requirements', 'type': 'str', 'value': 'update_requirements', + 'description': 'The requirements for the available update.'}, + {'name': 'Update Extra Info', 'type': 'str', 'value': 'update_extra_info', + 'description': 'Any extra info for the available update.'}, + {'name': 'Update Changelog Added', 'type': 'str', 'value': 'update_changelog_added', + 'description': 'The added changelog for the available update.'}, + {'name': 'Update Changelog Fixed', 'type': 'str', 'value': 'update_changelog_fixed', + 'description': 'The fixed changelog for the available update.'}, + ] + }, { 'category': 'Tautulli Update Available', 'parameters': [ - {'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'}, - {'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url', 'description': 'The release page URL on GitHub.'}, - {'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'}, - {'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'}, - {'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'}, - {'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'}, - {'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'}, + {'name': 'Tautulli Update Version', 'type': 'str', 'value': 'tautulli_update_version', + 'description': 'The available update version for Tautulli.'}, + {'name': 'Tautulli Update Release URL', 'type': 'str', 'value': 'tautulli_update_release_url', + 'description': 'The release page URL on GitHub.'}, + {'name': 'Tautulli Update Tar', 'type': 'str', 'value': 'tautulli_update_tar', + 'description': 'The tar download URL for the available update.'}, + {'name': 'Tautulli Update Zip', 'type': 'str', 'value': 'tautulli_update_zip', + 'description': 'The zip download URL for the available update.'}, + {'name': 'Tautulli Update Commit', 'type': 'str', 'value': 'tautulli_update_commit', + 'description': 'The commit hash for the available update.'}, + {'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', + 'description': 'The number of commits behind for the available update.'}, + {'name': 'Tautulli Update Changelog', 'type': 'str', 'value': 'tautulli_update_changelog', + 'description': 'The changelog for the available update.'}, ] - }, + }, ] NEWSLETTER_PARAMETERS = [ { 'category': 'Global', 'parameters': [ - {'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'}, - {'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'}, + {'name': 'Server Name', 'type': 'str', 'value': 'server_name', + 'description': 'The name of your Plex Server.'}, + {'name': 'Start Date', 'type': 'str', 'value': 'start_date', + 'description': 'The start date of the newsletter.'}, {'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'}, - {'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year of the start date of the newsletter.'}, - {'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'}, - {'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'}, - {'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'}, - {'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'}, - {'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'}, - {'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'}, - {'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'}, - {'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'}, - {'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'}, - {'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'}, - {'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url', 'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'}, - {'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', 'description': 'The unique identifier for the newsletter.'}, - {'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', 'description': 'The unique ID number for the newsletter agent.'}, - {'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', 'description': 'The unique ID name for the newsletter agent.'}, - {'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password', 'description': 'The password required to view the newsletter if enabled.'}, + {'name': 'Current Year', 'type': 'int', 'value': 'current_year', + 'description': 'The year of the start date of the newsletter.'}, + {'name': 'Current Month', 'type': 'int', 'value': 'current_month', + 'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'}, + {'name': 'Current Day', 'type': 'int', 'value': 'current_day', + 'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'}, + {'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', + 'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'}, + {'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', + 'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'}, + {'name': 'Current Second', 'type': 'int', 'value': 'current_second', + 'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'}, + {'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', + 'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'}, + {'name': 'Current Week', 'type': 'int', 'value': 'current_week', + 'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'}, + {'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', + 'description': 'The time frame included in the newsletter.'}, + {'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', + 'description': 'The time frame units included in the newsletter.'}, + {'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', + 'description': 'The self-hosted URL to the newsletter.'}, + {'name': 'Newsletter Static URL', 'type': 'str', 'value': 'newsletter_static_url', + 'description': 'The static self-hosted URL to the latest scheduled newsletter for the agent.'}, + {'name': 'Newsletter UUID', 'type': 'str', 'value': 'newsletter_uuid', + 'description': 'The unique identifier for the newsletter.'}, + {'name': 'Newsletter ID', 'type': 'int', 'value': 'newsletter_id', + 'description': 'The unique ID number for the newsletter agent.'}, + {'name': 'Newsletter ID Name', 'type': 'int', 'value': 'newsletter_id_name', + 'description': 'The unique ID name for the newsletter agent.'}, + {'name': 'Newsletter Password', 'type': 'str', 'value': 'newsletter_password', + 'description': 'The password required to view the newsletter if enabled.'}, ] - }, + }, { 'category': 'Recently Added', 'parameters': [ - {'name': 'Included Libraries', 'type': 'str', 'value': 'newsletter_libraries', 'description': 'The list of libraries included in the newsletter.'}, + {'name': 'Included Libraries', 'type': 'str', 'value': 'newsletter_libraries', + 'description': 'The list of libraries included in the newsletter.'}, ] } ] diff --git a/jellypy/config.py b/jellypy/config.py index 99a8ef9c..0ceb6ceb 100644 --- a/jellypy/config.py +++ b/jellypy/config.py @@ -13,25 +13,17 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import object -from future.builtins import str - import os import re import shutil -import time import threading +import time from configobj import ConfigObj, ParseError import jellypy -if jellypy.PYTHON2: - import helpers - import logger -else: - from jellypy import helpers - from jellypy import logger +from jellypy import helpers +from jellypy import logger def bool_int(value): @@ -114,7 +106,8 @@ _CONFIG_DEFINITIONS = { 'HOME_SECTIONS': (list, 'General', ['current_activity', 'watch_stats', 'library_stats', 'recently_added']), 'HOME_LIBRARY_CARDS': (list, 'General', ['first_run']), 'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', - 'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']), + 'popular_music', 'last_watched', 'top_users', 'top_platforms', + 'most_concurrent']), 'HOME_REFRESH_INTERVAL': (int, 'General', 10), 'HTTPS_CREATE_CERT': (int, 'General', 1), 'HTTPS_CERT': (str, 'General', ''), @@ -337,7 +330,7 @@ class Config(object): for key, subkeys in self._config.items(): for subkey, value in subkeys.items(): if isinstance(value, str) and len(value.strip()) > 5 and \ - subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS): + subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS): blacklist.add(value.strip()) logger._BLACKLIST_WORDS.update(blacklist) diff --git a/jellypy/database.py b/jellypy/database.py index 4877f490..718afa54 100644 --- a/jellypy/database.py +++ b/jellypy/database.py @@ -13,24 +13,16 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import object - import os -import sqlite3 import shutil +import sqlite3 import threading import time import jellypy -if jellypy.PYTHON2: - import helpers - import logger -else: - from jellypy import helpers - from jellypy import logger +from jellypy import helpers +from jellypy import logger FILENAME = "tautulli.db" db_lock = threading.Lock() @@ -455,8 +447,9 @@ class MonitorDatabase(object): if self.connection.total_changes == changes_before: trans_type = 'insert' insert_query = ( - "INSERT INTO " + table_name + " (" + ", ".join(list(value_dict.keys()) + list(key_dict.keys())) + ")" + - " VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")" + "INSERT INTO " + table_name + " (" + ", ".join( + list(value_dict.keys()) + list(key_dict.keys())) + ")" + + " VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")" ) try: self.action(insert_query, list(value_dict.values()) + list(key_dict.values())) @@ -470,4 +463,4 @@ class MonitorDatabase(object): # Get the last insert row id result = self.select_single(query='SELECT last_insert_rowid() AS last_id') if result: - return result.get('last_id', None) \ No newline at end of file + return result.get('last_id', None) diff --git a/jellypy/datafactory.py b/jellypy/datafactory.py index 6ba5a3ba..a521415e 100644 --- a/jellypy/datafactory.py +++ b/jellypy/datafactory.py @@ -15,32 +15,18 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import division -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import str -from future.builtins import object - import json from itertools import groupby import jellypy -if jellypy.PYTHON2: - import common - import database - import datatables - import helpers - import logger - import pmsconnect - import session -else: - from jellypy import common - from jellypy import database - from jellypy import datatables - from jellypy import helpers - from jellypy import logger - from jellypy import pmsconnect - from jellypy import session + +from jellypy import common +from jellypy import database +from jellypy import datatables +from jellypy import helpers +from jellypy import logger +from jellypy import pmsconnect +from jellypy import session class DataFactory(object): @@ -127,7 +113,7 @@ class DataFactory(object): 'GROUP_CONCAT(session_history.id) AS group_ids', 'NULL AS state', 'NULL AS session_key' - ] + ] if include_activity: table_name_union = 'sessions' @@ -180,7 +166,7 @@ class DataFactory(object): 'NULL AS group_ids', 'state', 'session_key' - ] + ] else: table_name_union = None @@ -343,7 +329,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_movies: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_movies: %s." % e) return None for item in result: @@ -395,7 +382,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_movies: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_movies: %s." % e) return None for item in result: @@ -444,7 +432,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_tv: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_tv: %s." % e) return None for item in result: @@ -496,7 +485,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_tv: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_tv: %s." % e) return None for item in result: @@ -546,7 +536,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_music: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_music: %s." % e) return None for item in result: @@ -599,7 +590,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_music: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: popular_music: %s." % e) return None for item in result: @@ -649,7 +641,8 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_users: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_users: %s." % e) return None for item in result: @@ -674,7 +667,7 @@ class DataFactory(object): 'title': '', 'platform': '', 'row_id': '' - } + } top_users.append(row) home_stats.append({'stat_id': stat, @@ -701,13 +694,15 @@ class DataFactory(object): 'LIMIT %s OFFSET %s ' % (time_range, group_by, sort_type, stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_platforms: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: top_platforms: %s." % e) return None for item in result: # Rename Mystery platform names platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']) - platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default') + platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), + 'default') row = {'total_plays': item['total_plays'], 'total_duration': item['total_duration'], @@ -759,7 +754,8 @@ class DataFactory(object): stats_count, stats_start) result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: last_watched: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: last_watched: %s." % e) return None for item in result: @@ -848,26 +844,27 @@ class DataFactory(object): title = 'Concurrent Transcodes' query = base_query \ - + 'AND session_history_media_info.transcode_decision = "transcode" ' + + 'AND session_history_media_info.transcode_decision = "transcode" ' result = monitor_db.select(query) if result: most_concurrent.append(calc_most_concurrent(title, result)) title = 'Concurrent Direct Streams' query = base_query \ - + 'AND session_history_media_info.transcode_decision = "copy" ' + + 'AND session_history_media_info.transcode_decision = "copy" ' result = monitor_db.select(query) if result: most_concurrent.append(calc_most_concurrent(title, result)) title = 'Concurrent Direct Plays' query = base_query \ - + 'AND session_history_media_info.transcode_decision = "direct play" ' + + 'AND session_history_media_info.transcode_decision = "direct play" ' result = monitor_db.select(query) if result: most_concurrent.append(calc_most_concurrent(title, result)) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_home_stats: most_concurrent: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_home_stats: most_concurrent: %s." % e) return None home_stats.append({'stat_id': stat, @@ -1577,14 +1574,14 @@ class DataFactory(object): key = item['parent_media_index'] if match_type == 'index' else item['parent_title'] parents.update({key: - {'rating_key': item['parent_rating_key'], - 'children': children} + {'rating_key': item['parent_rating_key'], + 'children': children} }) key = 0 if match_type == 'index' else item['grandparent_title'] grandparents.update({key: - {'rating_key': item['grandparent_rating_key'], - 'children': parents} + {'rating_key': item['grandparent_rating_key'], + 'children': parents} }) key_list = grandparents @@ -1619,16 +1616,20 @@ class DataFactory(object): if metadata: if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist': # check grandparent_rating_key (2 tables) - monitor_db.action('UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', - [new_key, old_key]) - monitor_db.action('UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', - [new_key, old_key]) + monitor_db.action( + 'UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', + [new_key, old_key]) + monitor_db.action( + 'UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', + [new_key, old_key]) elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album': # check parent_rating_key (2 tables) - monitor_db.action('UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?', - [new_key, old_key]) - monitor_db.action('UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?', - [new_key, old_key]) + monitor_db.action( + 'UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?', + [new_key, old_key]) + monitor_db.action( + 'UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?', + [new_key, old_key]) else: # check rating_key (2 tables) monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?', @@ -1661,7 +1662,7 @@ class DataFactory(object): genres = ";".join(metadata['genres']) labels = ";".join(metadata['labels']) - #logger.info("Tautulli DataFactory :: Updating metadata in the database for rating key: %s." % new_rating_key) + # logger.info("Tautulli DataFactory :: Updating metadata in the database for rating key: %s." % new_rating_key) monitor_db = database.MonitorDatabase() # Update the session_history_metadata table @@ -1874,7 +1875,8 @@ class DataFactory(object): query = 'SELECT * FROM recently_added WHERE rating_key = ?' result = monitor_db.select(query=query, args=[rating_key]) except Exception as e: - logger.warn("Tautulli DataFactory :: Unable to execute database query for get_recently_added_item: %s." % e) + logger.warn( + "Tautulli DataFactory :: Unable to execute database query for get_recently_added_item: %s." % e) return [] else: return [] diff --git a/jellypy/datatables.py b/jellypy/datatables.py index 34305dfb..2ca97943 100644 --- a/jellypy/datatables.py +++ b/jellypy/datatables.py @@ -13,20 +13,11 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import object - import re -import jellypy -if jellypy.PYTHON2: - import database - import helpers - import logger -else: - from jellypy import database - from jellypy import helpers - from jellypy import logger +from jellypy import database +from jellypy import helpers +from jellypy import logger class DataTables(object): @@ -224,7 +215,7 @@ class DataTables(object): args.append('%' + search_param + '%') if where: where = 'WHERE ' + where.rstrip(' OR ') - + return where, args # This method extracts column data from our column list diff --git a/jellypy/exceptions.py b/jellypy/exceptions.py index 96bb1032..61bb0a9c 100644 --- a/jellypy/exceptions.py +++ b/jellypy/exceptions.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals - -class PlexPyException(Exception): +class JellyPyException(Exception): """ Generic Tautulli Exception - should never be thrown, only subclassed """ diff --git a/jellypy/exporter.py b/jellypy/exporter.py index a199319b..2097eeaa 100644 --- a/jellypy/exporter.py +++ b/jellypy/exporter.py @@ -14,36 +14,24 @@ # # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . - -from __future__ import unicode_literals -from future.builtins import str -from backports import csv - +import csv import json import os -import requests import shutil import threading - from functools import partial, reduce from io import open from multiprocessing.dummy import Pool as ThreadPool +import requests + import jellypy -if jellypy.PYTHON2: - import database - import datatables - import helpers - import logger - import users - from plex import Plex -else: - from jellypy import database - from jellypy import datatables - from jellypy import helpers - from jellypy import logger - from jellypy import users - from jellypy.plex import Plex +from jellypy import database +from jellypy import datatables +from jellypy import helpers +from jellypy import logger +from jellypy import users +from jellypy.plex import Plex class Export(object): diff --git a/jellypy/graphs.py b/jellypy/graphs.py index 33b55102..4687d6ba 100644 --- a/jellypy/graphs.py +++ b/jellypy/graphs.py @@ -15,26 +15,15 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import range -from future.builtins import object - import datetime import jellypy -if jellypy.PYTHON2: - import common - import database - import logger - import libraries - import session -else: - from jellypy import common - from jellypy import database - from jellypy import logger - from jellypy import libraries - from jellypy import session + +from jellypy import common +from jellypy import database +from jellypy import logger +from jellypy import libraries +from jellypy import session class Graphs(object): @@ -224,7 +213,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_dayofweek: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_per_dayofweek: %s." % e) return None if jellypy.CONFIG.WEEK_START_MONDAY: @@ -339,7 +329,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_hourofday: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_per_hourofday: %s." % e) return None hours_list = ['00', '01', '02', '03', '04', '05', @@ -460,11 +451,11 @@ class Graphs(object): dt_today = datetime.date.today() dt = dt_today month_range = [dt] - for n in range(int(time_range)-1): - if not ((dt_today.month-n) % 12)-1: - dt = datetime.date(dt.year-1, 12, 1) + for n in range(int(time_range) - 1): + if not ((dt_today.month - n) % 12) - 1: + dt = datetime.date(dt.year - 1, 12, 1) else: - dt = datetime.date(dt.year, dt.month-1, 1) + dt = datetime.date(dt.year, dt.month - 1, 1) month_range.append(dt) categories = [] @@ -578,7 +569,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_platforms: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_platforms: %s." % e) return None categories = [] @@ -682,7 +674,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_users: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_by_top_10_users: %s." % e) return None categories = [] @@ -787,7 +780,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_stream_type: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_per_stream_type: %s." % e) return None # create our date range as some days may not have any data @@ -893,7 +887,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_source_resolution: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_by_source_resolution: %s." % e) return None categories = [] @@ -954,7 +949,7 @@ class Graphs(object): 'THEN 1 ELSE 0 END) AS dp_count, ' \ 'SUM(CASE WHEN session_history_media_info.transcode_decision = "copy" ' \ 'THEN 1 ELSE 0 END) AS ds_count, ' \ - 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" '\ + 'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \ 'THEN 1 ELSE 0 END) AS tc_count, ' \ 'COUNT(session_history.id) AS total_count ' \ 'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \ @@ -1003,7 +998,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_by_stream_resolution: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_total_plays_by_stream_resolution: %s." % e) return None categories = [] @@ -1092,7 +1088,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_platforms: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_platforms: %s." % e) return None categories = [] @@ -1190,7 +1187,8 @@ class Graphs(object): result = monitor_db.select(query) except Exception as e: - logger.warn("Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_users: %s." % e) + logger.warn( + "Tautulli Graphs :: Unable to execute database query for get_stream_type_by_top_10_users: %s." % e) return None categories = [] diff --git a/jellypy/helpers.py b/jellypy/helpers.py index b57a7cac..b17d0614 100644 --- a/jellypy/helpers.py +++ b/jellypy/helpers.py @@ -15,28 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import division -from __future__ import unicode_literals - -from future.builtins import zip -from future.builtins import str - -import arrow import base64 -import cloudinary -from cloudinary.api import delete_resources_by_tag -from cloudinary.uploader import upload -from cloudinary.utils import cloudinary_url -from collections import OrderedDict import datetime -from functools import reduce, wraps import hashlib import imghdr -from future.moves.itertools import islice, zip_longest -import ipwhois -import ipwhois.exceptions -import ipwhois.utils -from IPy import IP import json import math import operator @@ -48,21 +30,27 @@ import string import sys import time import unicodedata -from future.moves.urllib.parse import urlencode +from collections import OrderedDict +from functools import reduce, wraps from xml.dom import minidom + +import arrow +import cloudinary +import ipwhois +import ipwhois.exceptions +import ipwhois.utils import xmltodict +from IPy import IP +from cloudinary.api import delete_resources_by_tag +from cloudinary.uploader import upload +from cloudinary.utils import cloudinary_url import jellypy -if jellypy.PYTHON2: - import common - import logger - import request - from api2 import API2 -else: - from jellypy import common - from jellypy import logger - from jellypy import request - from jellypy.api2 import API2 + +from jellypy import common +from jellypy import logger +from jellypy import request +from jellypy.api2 import API2 def addtoapi(*dargs, **dkwargs): @@ -77,6 +65,7 @@ def addtoapi(*dargs, **dkwargs): @addtoapi() """ + def rd(function): @wraps(function) def wrapper(*args, **kwargs): @@ -107,7 +96,6 @@ def checked(variable): def radio(variable, pos): - if variable == pos: return 'Checked' else: @@ -161,7 +149,6 @@ def latinToAscii(unicrap, replace=False): def convert_milliseconds(ms): - seconds = ms // 1000 gmtime = time.gmtime(seconds) if seconds > 3600: @@ -173,7 +160,6 @@ def convert_milliseconds(ms): def convert_milliseconds_to_minutes(ms): - if str(ms).isdigit(): seconds = float(ms) / 1000 minutes = round(seconds / 60, 0) @@ -184,7 +170,6 @@ def convert_milliseconds_to_minutes(ms): def convert_seconds(s): - gmtime = time.gmtime(s) if s > 3600: minutes = time.strftime("%H:%M:%S", gmtime) @@ -195,7 +180,6 @@ def convert_seconds(s): def convert_seconds_to_minutes(s): - if str(s).isdigit(): minutes = round(float(s) / 60, 0) @@ -302,7 +286,6 @@ def format_timedelta_Hms(td): def get_age(date): - try: split_date = date.split('-') except: @@ -317,7 +300,6 @@ def get_age(date): def bytes_to_mb(bytes): - mb = float(bytes) / 1048576 size = '%.1f MB' % mb return size @@ -355,7 +337,6 @@ def piratesize(size): def replace_all(text, dic, normalize=False): - if not text: return '' @@ -382,7 +363,6 @@ def replace_illegal_chars(string, type="file"): def cleanName(string): - pass1 = latinToAscii(string).lower() out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', pass1).encode('utf-8') @@ -390,7 +370,6 @@ def cleanName(string): def cleanTitle(title): - title = re.sub('[\.\-\/\_]', ' ', title).lower() # Strip out extra whitespace @@ -442,7 +421,8 @@ def split_path(f): def extract_logline(s): # Default log format - pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s*\:\:\s(?P.*?)\s\:\s(?P.*)', re.VERBOSE) + pattern = re.compile(r'(?P.*?)\s\-\s(?P.*?)\s*\:\:\s(?P.*?)\s\:\s(?P.*)', + re.VERBOSE) match = pattern.match(s) if match: timestamp = match.group("timestamp") @@ -526,7 +506,6 @@ def convert_xml_to_dict(xml): def get_percent(value1, value2): - value1 = cast_to_float(value1) value2 = cast_to_float(value2) @@ -646,11 +625,14 @@ def sort_helper(k, sort_key, sort_keys): def sanitize_out(*dargs, **dkwargs): """ Helper decorator that sanitized the output """ + def rd(function): @wraps(function) def wrapper(*args, **kwargs): return sanitize(function(*args, **kwargs)) + return wrapper + return rd @@ -697,7 +679,6 @@ def is_valid_ip(address): def whois_lookup(ip_address): - nets = [] err = None try: @@ -708,7 +689,7 @@ def whois_lookup(ip_address): for net in nets: net['country'] = countries.get(net['country']) if net['postal_code']: - net['postal_code'] = net['postal_code'].replace('-', ' ') + net['postal_code'] = net['postal_code'].replace('-', ' ') except ValueError as e: err = 'Invalid IP address provided: %s.' % ip_address except ipwhois.exceptions.IPDefinedError as e: @@ -777,7 +758,8 @@ def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''): delete_hash = imgur_response_data.get('deletehash', '') else: if err_msg: - logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Imgur: {}".format(img_title, fallback, err_msg)) + logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Imgur: {}".format(img_title, fallback, + err_msg)) else: logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Imgur.".format(img_title, fallback)) @@ -790,7 +772,8 @@ def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''): def delete_from_imgur(delete_hash, img_title='', fallback=''): """ Deletes an image from Imgur """ if not jellypy.CONFIG.IMGUR_CLIENT_ID: - logger.error("Tautulli Helpers :: Cannot delete image from Imgur. No Imgur client id specified in the settings.") + logger.error( + "Tautulli Helpers :: Cannot delete image from Imgur. No Imgur client id specified in the settings.") return False headers = {'Authorization': 'Client-ID %s' % jellypy.CONFIG.IMGUR_CLIENT_ID} @@ -803,7 +786,9 @@ def delete_from_imgur(delete_hash, img_title='', fallback=''): return True else: if err_msg: - logger.error("Tautulli Helpers :: Unable to delete image '{}' ({}) from Imgur: {}".format(img_title, fallback, err_msg)) + logger.error( + "Tautulli Helpers :: Unable to delete image '{}' ({}) from Imgur: {}".format(img_title, fallback, + err_msg)) else: logger.error("Tautulli Helpers :: Unable to delete image '{}' ({}) from Imgur.".format(img_title, fallback)) return False @@ -814,7 +799,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''): img_url = '' if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET: - logger.error("Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.") + logger.error( + "Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.") return img_url cloudinary.config( @@ -837,7 +823,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''): logger.debug("Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback)) img_url = response.get('url', '') except Exception as e: - logger.error("Tautulli Helpers :: Unable to upload image '{}' ({}) to Cloudinary: {}".format(img_title, fallback, e)) + logger.error( + "Tautulli Helpers :: Unable to upload image '{}' ({}) to Cloudinary: {}".format(img_title, fallback, e)) return img_url @@ -845,7 +832,8 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''): def delete_from_cloudinary(rating_key=None, delete_all=False): """ Deletes an image from Cloudinary """ if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET: - logger.error("Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.") + logger.error( + "Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.") return False cloudinary.config( @@ -871,7 +859,8 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100, url = '' if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET: - logger.error("Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.") + logger.error( + "Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.") return url cloudinary.config( @@ -903,7 +892,8 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100, url, options = cloudinary_url('{}_{}'.format(fallback, rating_key), **img_options) logger.debug("Tautulli Helpers :: Image '{}' ({}) transformed on Cloudinary.".format(img_title, fallback)) except Exception as e: - logger.error("Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e)) + logger.error( + "Tautulli Helpers :: Unable to transform image '{}' ({}) on Cloudinary: {}".format(img_title, fallback, e)) return url @@ -987,7 +977,7 @@ def human_file_size(bytes, si=True): else: return bytes - #thresh = 1000 if si else 1024 + # thresh = 1000 if si else 1024 thresh = 1024 # Always divide by 2^10 but display SI units if bytes < thresh: return str(bytes) + ' B' @@ -1064,7 +1054,7 @@ def parse_condition_logic_string(s, num_cond=0): stack.pop() nest_and -= 1 - elif bool_next and x == 'and' and i < len(tokens)-1: + elif bool_next and x == 'and' and i < len(tokens) - 1: stack[-1].append([]) stack.append(stack[-1][-1]) stack[-1].append(stack[-2].pop(-2)) @@ -1075,7 +1065,7 @@ def parse_condition_logic_string(s, num_cond=0): close_bracket_next = False nest_and += 1 - elif bool_next and x == 'or' and i < len(tokens)-1: + elif bool_next and x == 'or' and i < len(tokens) - 1: stack[-1].append(x) cond_next = True bool_next = False @@ -1395,7 +1385,7 @@ def dict_merge(a, b, path=None): return a -#https://stackoverflow.com/a/26853961 +# https://stackoverflow.com/a/26853961 def dict_update(*dict_args): """ Given any number of dictionaries, shallow copy and merge into a new dict, diff --git a/jellypy/http_handler.py b/jellypy/http_handler.py index cfadb964..8711ed2e 100644 --- a/jellypy/http_handler.py +++ b/jellypy/http_handler.py @@ -14,25 +14,16 @@ # # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . - -from __future__ import unicode_literals -from future.builtins import object -from future.builtins import str - -from multiprocessing.dummy import Pool as ThreadPool -from future.moves.urllib.parse import urljoin +from multiprocessing.pool import ThreadPool +from urllib.parse import urljoin import certifi import requests import urllib3 import jellypy -if jellypy.PYTHON2: - import helpers - import logger -else: - from jellypy import helpers - from jellypy import logger +from jellypy import helpers +from jellypy import logger class HTTPHandler(object): diff --git a/jellypy/libraries.py b/jellypy/libraries.py index 80ad13a7..6fcf8174 100644 --- a/jellypy/libraries.py +++ b/jellypy/libraries.py @@ -15,37 +15,21 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import next -from future.builtins import object import json import os import jellypy -if jellypy.PYTHON2: - import common - import database - import datatables - import helpers - import logger - import plextv - import pmsconnect - import session - import users - from plex import Plex -else: - from jellypy import common - from jellypy import database - from jellypy import datatables - from jellypy import helpers - from jellypy import logger - from jellypy import plextv - from jellypy import pmsconnect - from jellypy import session - from jellypy import users - from jellypy.plex import Plex +from jellypy import common +from jellypy import database +from jellypy import datatables +from jellypy import helpers +from jellypy import logger +from jellypy import plextv +from jellypy import pmsconnect +from jellypy import session +from jellypy import users +from jellypy.plex import Plex def refresh_libraries(): @@ -378,9 +362,10 @@ class Libraries(object): join_tables=['session_history_metadata', 'session_history', 'session_history_media_info'], - join_evals=[['session_history_metadata.section_id', 'library_sections.section_id'], - ['session_history_metadata.id', 'session_history.id'], - ['session_history_metadata.id', 'session_history_media_info.id']], + join_evals=[ + ['session_history_metadata.section_id', 'library_sections.section_id'], + ['session_history_metadata.id', 'session_history.id'], + ['session_history_metadata.id', 'session_history_media_info.id']], kwargs=kwargs) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_list: %s." % e) @@ -452,7 +437,8 @@ class Libraries(object): return dict - def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False, kwargs=None): + def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False, + kwargs=None): default_return = {'recordsFiltered': 0, 'recordsTotal': 0, 'draw': 0, @@ -505,7 +491,8 @@ class Libraries(object): 'GROUP BY session_history.%s ' % (count_by, group_by) result = monitor_db.select(query, args=[section_id]) except Exception as e: - logger.warn("Tautulli Libraries :: Unable to execute database query for get_datatables_media_info2: %s." % e) + logger.warn( + "Tautulli Libraries :: Unable to execute database query for get_datatables_media_info2: %s." % e) return default_return watched_list = {} @@ -522,8 +509,8 @@ class Libraries(object): rows = json.load(inFile) library_count = len(rows) except IOError as e: - #logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key) - #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key) + # logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key) + # logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key) pass elif section_id: try: @@ -532,8 +519,8 @@ class Libraries(object): rows = json.load(inFile) library_count = len(rows) except IOError as e: - #logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id) - #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id) + # logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id) + # logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id) pass # If no cache was imported, get all library children items @@ -594,7 +581,8 @@ class Libraries(object): # Cache the media info to a json file if rating_key: try: - outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key)) + outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, + 'media_info_%s-%s.json' % (section_id, rating_key)) with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: @@ -622,14 +610,14 @@ class Libraries(object): # Get datatables JSON data if kwargs.get('json_data'): json_data = helpers.process_json_kwargs(json_kwargs=kwargs.get('json_data')) - #print json_data + # print json_data # Search results search_value = json_data['search']['value'].lower() if search_value: searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] + ['title'] for row in rows: - for k,v in row.items(): + for k, v in row.items(): if k in searchable_columns and search_value in v.lower(): results.append(row) break @@ -649,7 +637,9 @@ class Libraries(object): elif sort_key in ('file_size', 'bitrate', 'added_at', 'last_played', 'play_count'): results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse) elif sort_key == 'video_resolution': - results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse) + results = sorted(results, + key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), + reverse=reverse) else: results = sorted(results, key=lambda k: k[sort_key].lower(), reverse=reverse) @@ -692,14 +682,14 @@ class Libraries(object): rows = [] # Import media info cache from json file if rating_key: - #logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key) + # logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key) try: inFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key)) with open(inFilePath, 'r') as inFile: rows = json.load(inFile) except IOError as e: - #logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key) - #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key) + # logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key) + # logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key) pass elif section_id: logger.debug("Tautulli Libraries :: Getting file sizes for section_id %s." % section_id) @@ -708,8 +698,8 @@ class Libraries(object): with open(inFilePath, 'r') as inFile: rows = json.load(inFile) except IOError as e: - #logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id) - #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id) + # logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id) + # logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id) pass # Get the total file size for each item @@ -727,7 +717,7 @@ class Libraries(object): media_info = media_part_info = {} if 'media_info' in child_metadata and len(child_metadata['media_info']) > 0: media_info = child_metadata['media_info'][0] - if 'parts' in media_info and len (media_info['parts']) > 0: + if 'parts' in media_info and len(media_info['parts']) > 0: media_part_info = next((p for p in media_info['parts'] if p['selected']), media_info['parts'][0]) @@ -742,22 +732,25 @@ class Libraries(object): with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: - logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key) + logger.debug( + "Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key) elif section_id: try: outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s.json' % section_id) with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: - logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for section_id %s." % section_id) + logger.debug( + "Tautulli Libraries :: Unable to create cache file with file sizes for section_id %s." % section_id) if rating_key: - #logger.debug("Tautulli Libraries :: File sizes updated for rating_key %s." % rating_key) + # logger.debug("Tautulli Libraries :: File sizes updated for rating_key %s." % rating_key) pass elif section_id: logger.debug("Tautulli Libraries :: File sizes updated for section_id %s." % section_id) return True + def set_config(self, section_id=None, custom_thumb='', custom_art='', do_notify=1, keep_history=1, do_notify_created=1): if section_id: @@ -856,8 +849,9 @@ class Libraries(object): return library_details else: - logger.warn("Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh." - % section_id) + logger.warn( + "Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh." + % section_id) # Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet refresh_libraries() @@ -1016,36 +1010,36 @@ class Libraries(object): result = [] for row in result: - if row['media_type'] == 'episode' and row['parent_thumb']: - thumb = row['parent_thumb'] - elif row['media_type'] == 'episode': - thumb = row['grandparent_thumb'] - else: - thumb = row['thumb'] + if row['media_type'] == 'episode' and row['parent_thumb']: + thumb = row['parent_thumb'] + elif row['media_type'] == 'episode': + thumb = row['grandparent_thumb'] + else: + thumb = row['thumb'] - recent_output = {'row_id': row['id'], - 'media_type': row['media_type'], - 'rating_key': row['rating_key'], - 'parent_rating_key': row['parent_rating_key'], - 'grandparent_rating_key': row['grandparent_rating_key'], - 'title': row['title'], - 'parent_title': row['parent_title'], - 'grandparent_title': row['grandparent_title'], - 'original_title': row['original_title'], - 'thumb': thumb, - 'media_index': row['media_index'], - 'parent_media_index': row['parent_media_index'], - 'year': row['year'], - 'originally_available_at': row['originally_available_at'], - 'live': row['live'], - 'guid': row['guid'], - 'time': row['started'], - 'user': row['user'], - 'section_id': row['section_id'], - 'content_rating': row['content_rating'], - 'labels': row['labels'].split(';') if row['labels'] else (), - } - recently_watched.append(recent_output) + recent_output = {'row_id': row['id'], + 'media_type': row['media_type'], + 'rating_key': row['rating_key'], + 'parent_rating_key': row['parent_rating_key'], + 'grandparent_rating_key': row['grandparent_rating_key'], + 'title': row['title'], + 'parent_title': row['parent_title'], + 'grandparent_title': row['grandparent_title'], + 'original_title': row['original_title'], + 'thumb': thumb, + 'media_index': row['media_index'], + 'parent_media_index': row['parent_media_index'], + 'year': row['year'], + 'originally_available_at': row['originally_available_at'], + 'live': row['live'], + 'guid': row['guid'], + 'time': row['started'], + 'user': row['user'], + 'section_id': row['section_id'], + 'content_rating': row['content_rating'], + 'labels': row['labels'].split(';') if row['labels'] else (), + } + recently_watched.append(recent_output) return session.mask_session_info(recently_watched) diff --git a/jellypy/lock.py b/jellypy/lock.py index a53b4ebf..acdd9c71 100644 --- a/jellypy/lock.py +++ b/jellypy/lock.py @@ -14,19 +14,11 @@ # # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . - -from __future__ import unicode_literals -from future.builtins import object - -import future.moves.queue as queue -import time +import queue import threading +import time -import jellypy -if jellypy.PYTHON2: - import logger -else: - from jellypy import logger +from jellypy import logger class TimedLock(object): diff --git a/jellypy/log_reader.py b/jellypy/log_reader.py index e48508c3..212ea372 100644 --- a/jellypy/log_reader.py +++ b/jellypy/log_reader.py @@ -15,22 +15,15 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals +import os from io import open -import os - import jellypy -if jellypy.PYTHON2: - import helpers - import logger -else: - from jellypy import helpers - from jellypy import logger +from jellypy import helpers +from jellypy import logger def get_log_tail(window=20, parsed=True, log_type="server"): - if jellypy.CONFIG.PMS_LOGS_FOLDER: log_file = "" if log_type == "server": @@ -76,6 +69,7 @@ def get_log_tail(window=20, parsed=True, log_type="server"): return log_lines + # http://stackoverflow.com/a/13790289/2405162 def tail(f, lines=1, _buffer=4098): """Tail a file and get X lines from the end""" @@ -105,4 +99,4 @@ def tail(f, lines=1, _buffer=4098): # next X bytes block_counter -= 1 - return lines_found[-lines:] \ No newline at end of file + return lines_found[-lines:] diff --git a/jellypy/logger.py b/jellypy/logger.py index 1a1e5263..60a2e588 100644 --- a/jellypy/logger.py +++ b/jellypy/logger.py @@ -15,13 +15,7 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from logutils.queue import QueueHandler, QueueListener -from logging import handlers - -import cherrypy import contextlib import errno import logging @@ -31,15 +25,15 @@ import re import sys import threading import traceback +from logging import handlers + +import cherrypy +from logutils.queue import QueueHandler, QueueListener import jellypy -if jellypy.PYTHON2: - import helpers - from config import _BLACKLIST_KEYS, _WHITELIST_KEYS -else: - from jellypy import helpers - from jellypy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS +from jellypy import helpers +from jellypy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS # These settings are for file logging only FILENAME = "tautulli.log" @@ -67,8 +61,8 @@ def blacklist_config(config): for key, value in config.items(): if isinstance(value, str) and len(value.strip()) > 5 and \ - key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or - any(bk in key.upper() for bk in _BLACKLIST_KEYS)): + key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or + any(bk in key.upper() for bk in _BLACKLIST_KEYS)): blacklist.add(value.strip()) _BLACKLIST_WORDS.update(blacklist) @@ -78,9 +72,10 @@ class NoThreadFilter(logging.Filter): """ Log filter for the current thread """ + def __init__(self, threadName): super(NoThreadFilter, self).__init__() - + self.threadName = threadName def filter(self, record): @@ -92,6 +87,7 @@ class BlacklistFilter(logging.Filter): """ Log filter for blacklisted tokens and passwords """ + def __init__(self): super(BlacklistFilter, self).__init__() @@ -125,6 +121,7 @@ class RegexFilter(logging.Filter): """ Base class for regex log filter """ + def __init__(self): super(RegexFilter, self).__init__() @@ -165,6 +162,7 @@ class PublicIPFilter(RegexFilter): """ Log filter for public IP addresses """ + def __init__(self): super(PublicIPFilter, self).__init__() @@ -182,6 +180,7 @@ class EmailFilter(RegexFilter): """ Log filter for email addresses """ + def __init__(self): super(EmailFilter, self).__init__() @@ -198,6 +197,7 @@ class PlexTokenFilter(RegexFilter): """ Log filter for X-Plex-Token """ + def __init__(self): super(PlexTokenFilter, self).__init__() @@ -309,11 +309,13 @@ def initLogger(console=False, log_dir=False, verbose=False): # Setup file logger if log_dir: - file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%Y-%m-%d %H:%M:%S') + file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', + '%Y-%m-%d %H:%M:%S') # Main Tautulli logger filename = os.path.join(log_dir, FILENAME) - file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8') + file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, + encoding='utf-8') file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) @@ -322,7 +324,8 @@ def initLogger(console=False, log_dir=False, verbose=False): # Tautulli API logger filename = os.path.join(log_dir, FILENAME_API) - file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8') + file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, + encoding='utf-8') file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) @@ -330,7 +333,8 @@ def initLogger(console=False, log_dir=False, verbose=False): # Tautulli websocket logger filename = os.path.join(log_dir, FILENAME_PLEX_WEBSOCKET) - file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, encoding='utf-8') + file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES, + encoding='utf-8') file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) @@ -338,7 +342,8 @@ def initLogger(console=False, log_dir=False, verbose=False): # Setup console logger if console: - console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%Y-%m-%d %H:%M:%S') + console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', + '%Y-%m-%d %H:%M:%S') console_handler = logging.StreamHandler() console_handler.setFormatter(console_formatter) console_handler.setLevel(logging.DEBUG) @@ -407,6 +412,7 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True raise except: excepthook(*sys.exc_info()) + self.run = new_run # Monkey patch the run() by monkey patching the __init__ method diff --git a/jellypy/macos.py b/jellypy/macos.py index a6d00790..92abe9f2 100644 --- a/jellypy/macos.py +++ b/jellypy/macos.py @@ -16,13 +16,14 @@ # along with Tautulli. If not, see . import os +import plistlib import subprocess import sys -import plistlib try: import AppKit import Foundation + HAS_PYOBJC = True except ImportError: HAS_PYOBJC = False @@ -31,14 +32,10 @@ if HAS_PYOBJC: import rumps import jellypy -if jellypy.PYTHON2: - import common - import logger - import versioncheck -else: - from jellypy import common - from jellypy import logger - from jellypy import versioncheck + +from jellypy import common +from jellypy import logger +from jellypy import versioncheck class MacOSSystemTray(object): diff --git a/jellypy/mobile_app.py b/jellypy/mobile_app.py index bae39ef8..7ac5527e 100644 --- a/jellypy/mobile_app.py +++ b/jellypy/mobile_app.py @@ -15,22 +15,13 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str - -import requests import threading -import jellypy -if jellypy.PYTHON2: - import database - import helpers - import logger -else: - from jellypy import database - from jellypy import helpers - from jellypy import logger +import requests +from jellypy import database +from jellypy import helpers +from jellypy import logger TEMP_DEVICE_TOKEN = None INVALIDATE_TIMER = None @@ -118,7 +109,8 @@ def get_mobile_device_config(mobile_device_id=None): if str(mobile_device_id).isdigit(): mobile_device_id = int(mobile_device_id) else: - logger.error("Tautulli MobileApp :: Unable to retrieve mobile device config: invalid mobile_device_id %s." % mobile_device_id) + logger.error( + "Tautulli MobileApp :: Unable to retrieve mobile device config: invalid mobile_device_id %s." % mobile_device_id) return None db = database.MonitorDatabase() @@ -132,7 +124,8 @@ def set_mobile_device_config(mobile_device_id=None, **kwargs): if str(mobile_device_id).isdigit(): mobile_device_id = int(mobile_device_id) else: - logger.error("Tautulli MobileApp :: Unable to set exisiting mobile device: invalid mobile_device_id %s." % mobile_device_id) + logger.error( + "Tautulli MobileApp :: Unable to set exisiting mobile device: invalid mobile_device_id %s." % mobile_device_id) return False keys = {'id': mobile_device_id} diff --git a/jellypy/newsletter_handler.py b/jellypy/newsletter_handler.py index acc1eb75..7f703c7a 100644 --- a/jellypy/newsletter_handler.py +++ b/jellypy/newsletter_handler.py @@ -15,26 +15,17 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals - -from io import open +import email.utils import os +from io import open from apscheduler.triggers.cron import CronTrigger -import email.utils import jellypy -if jellypy.PYTHON2: - import database - import helpers - import logger - import newsletters -else: - from jellypy import database - from jellypy import helpers - from jellypy import logger - from jellypy import newsletters - +from jellypy import database +from jellypy import helpers +from jellypy import logger +from jellypy import newsletters NEWSLETTER_SCHED = None @@ -59,7 +50,8 @@ def schedule_newsletters(newsletter_id=None): if newsletter['active']: schedule_newsletter_job('newsletter-{}'.format(newsletter['id']), name=newsletter_job_name, - func=add_newsletter_each, args=[newsletter['id'], 'on_cron'], cron=newsletter['cron']) + func=add_newsletter_each, args=[newsletter['id'], 'on_cron'], + cron=newsletter['cron']) else: schedule_newsletter_job('newsletter-{}'.format(newsletter['id']), name=newsletter_job_name, remove_job=True) @@ -143,7 +135,6 @@ def notify(newsletter_id=None, notify_action=None, **kwargs): def set_notify_state(newsletter, notify_action, subject, body, message, filename, start_date, end_date, start_time, end_time, newsletter_uuid, email_msg_id): - if newsletter and notify_action: db = database.MonitorDatabase() @@ -220,6 +211,7 @@ def get_newsletter(newsletter_uuid=None, newsletter_id_name=None): newsletter = n_file.read() return newsletter except OSError as e: - logger.error("Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e)) + logger.error( + "Tautulli NewsletterHandler :: Failed to retrieve newsletter '%s': %s" % (newsletter_uuid, e)) else: logger.warn("Tautulli NewsletterHandler :: Newsletter file '%s' is missing." % newsletter_file) diff --git a/jellypy/newsletters.py b/jellypy/newsletters.py index 4c8c7db7..aef9aafc 100644 --- a/jellypy/newsletters.py +++ b/jellypy/newsletters.py @@ -15,40 +15,25 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import str -from future.builtins import object - -import arrow -from collections import OrderedDict import json -from itertools import groupby -from mako.lookup import TemplateLookup -from mako import exceptions import os import re +from collections import OrderedDict +from itertools import groupby + +import arrow +from mako import exceptions +from mako.lookup import TemplateLookup import jellypy -if jellypy.PYTHON2: - import common - import database - import helpers - import libraries - import logger - import newsletter_handler - import pmsconnect - from notifiers import send_notification, EMAIL -else: - from jellypy import common - from jellypy import database - from jellypy import helpers - from jellypy import libraries - from jellypy import logger - from jellypy import newsletter_handler - from jellypy import pmsconnect - from jellypy.notifiers import send_notification, EMAIL - +from jellypy import common +from jellypy import database +from jellypy import helpers +from jellypy import libraries +from jellypy import logger +from jellypy import newsletter_handler +from jellypy import pmsconnect +from jellypy.notifiers import send_notification, EMAIL AGENT_IDS = { 'recently_added': 0 @@ -512,7 +497,8 @@ class Newsletter(object): self.newsletter = self.generate_newsletter() if self.template_error: - logger.error("Tautulli Newsletters :: %s newsletter failed to render template. Newsletter not sent." % self.NAME) + logger.error( + "Tautulli Newsletters :: %s newsletter failed to render template. Newsletter not sent." % self.NAME) return False if not self._has_data(): @@ -540,7 +526,7 @@ class Newsletter(object): for line in self.newsletter.splitlines(): if '' not in line: n_file.write((line + '\r\n').encode('utf-8')) - #n_file.write(line.strip()) + # n_file.write(line.strip()) logger.info("Tautulli Newsletters :: %s newsletter saved to '%s'" % (self.NAME, newsletter_file)) except OSError as e: @@ -578,10 +564,10 @@ class Newsletter(object): ) elif self.config['notifier_id']: return send_notification( - notifier_id=self.config['notifier_id'], - subject=self.subject_formatted, - body=self.body_formatted - ) + notifier_id=self.config['notifier_id'], + subject=self.subject_formatted, + body=self.body_formatted + ) def build_params(self): parameters = self._build_params() @@ -628,7 +614,8 @@ class Newsletter(object): try: subject = custom_formatter.format(str(self.subject), **self.parameters) except LookupError as e: - logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter subject. Using fallback." % e) + logger.error( + "Tautulli Newsletter :: Unable to parse parameter %s in newsletter subject. Using fallback." % e) subject = str(self._DEFAULT_SUBJECT).format(**self.parameters) except Exception as e: logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e) @@ -646,7 +633,8 @@ class Newsletter(object): try: message = custom_formatter.format(str(self.message), **self.parameters) except LookupError as e: - logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter message. Using fallback." % e) + logger.error( + "Tautulli Newsletter :: Unable to parse parameter %s in newsletter message. Using fallback." % e) message = str(self._DEFAULT_MESSAGE).format(**self.parameters) except Exception as e: logger.error("Tautulli Newsletter :: Unable to parse custom newsletter message: %s. Using fallback." % e) @@ -661,7 +649,8 @@ class Newsletter(object): try: filename = custom_formatter.format(str(self.filename), **self.parameters) except LookupError as e: - logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e) + logger.error( + "Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e) filename = str(self._DEFAULT_FILENAME).format(**self.parameters) except Exception as e: logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e) @@ -821,7 +810,8 @@ class RecentlyAdded(Newsletter): from jellypy.notification_handler import get_img_info, set_hash_image_info if not self.config['incl_libraries']: - logger.warn("Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME) + logger.warn( + "Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME) media_types = set() for s in self._get_sections(): diff --git a/jellypy/notification_handler.py b/jellypy/notification_handler.py index 0b89a39b..aab495dc 100644 --- a/jellypy/notification_handler.py +++ b/jellypy/notification_handler.py @@ -15,60 +15,40 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . - -from __future__ import division -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import map -from future.builtins import str -from future.builtins import range +import hashlib +import json +import os +import re +import threading +import time +from collections import Counter, defaultdict +from functools import partial +from itertools import groupby +from operator import itemgetter +from string import Formatter import arrow import bleach -from collections import Counter, defaultdict -from functools import partial -import hashlib -from itertools import groupby -import json -from operator import itemgetter -import os -import re -from string import Formatter -import threading -import time - import musicbrainzngs import jellypy -if jellypy.PYTHON2: - import activity_processor - import common - import database - import datafactory - import logger - import helpers - import notifiers - import pmsconnect - import request - from newsletter_handler import notify as notify_newsletter -else: - from jellypy import activity_processor - from jellypy import common - from jellypy import database - from jellypy import datafactory - from jellypy import logger - from jellypy import helpers - from jellypy import notifiers - from jellypy import pmsconnect - from jellypy import request - from jellypy.newsletter_handler import notify as notify_newsletter +from jellypy import activity_processor +from jellypy import common +from jellypy import database +from jellypy import datafactory +from jellypy import logger +from jellypy import helpers +from jellypy import notifiers +from jellypy import pmsconnect +from jellypy import request +from jellypy.newsletter_handler import notify as notify_newsletter def process_queue(): queue = jellypy.NOTIFY_QUEUE while True: params = queue.get() - + if params is None: break elif params: @@ -88,14 +68,16 @@ def process_queue(): def start_threads(num_threads=1): - logger.info("Tautulli NotificationHandler :: Starting background notification handler ({} threads).".format(num_threads)) + logger.info( + "Tautulli NotificationHandler :: Starting background notification handler ({} threads).".format(num_threads)) for x in range(num_threads): thread = threading.Thread(target=process_queue) thread.daemon = True thread.start() -def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, manual_trigger=False, **kwargs): +def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, manual_trigger=False, + **kwargs): if not notify_action: logger.debug("Tautulli NotificationHandler :: Notify called but no action received.") return @@ -119,7 +101,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti if notifiers_enabled and (manual_trigger or conditions): if manual_trigger: - logger.debug("Tautulli NotificationHandler :: Notifiers enabled for notify_action '%s' (manual trigger)." % notify_action) + logger.debug( + "Tautulli NotificationHandler :: Notifiers enabled for notify_action '%s' (manual trigger)." % notify_action) if stream_data or timeline_data: # Build the notification parameters @@ -150,7 +133,9 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti data.update(kwargs) jellypy.NOTIFY_QUEUE.put(data) else: - logger.debug("Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % notifier['id']) + logger.debug( + "Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % + notifier['id']) # Add on_concurrent and on_newdevice to queue if action is on_play if notify_action == 'on_play': @@ -187,7 +172,8 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None): user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']] if jellypy.CONFIG.NOTIFY_CONCURRENT_BY_IP: - evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD + evaluated = len( + Counter(s['ip_address'] for s in user_sessions)) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD else: evaluated = len(user_sessions) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD @@ -202,8 +188,10 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None): if notify_action == 'on_stop': evaluated = (jellypy.CONFIG.NOTIFY_CONSECUTIVE or - (stream_data['media_type'] == 'movie' and progress_percent < jellypy.CONFIG.MOVIE_WATCHED_PERCENT) or - (stream_data['media_type'] == 'episode' and progress_percent < jellypy.CONFIG.TV_WATCHED_PERCENT)) + (stream_data[ + 'media_type'] == 'movie' and progress_percent < jellypy.CONFIG.MOVIE_WATCHED_PERCENT) or + (stream_data[ + 'media_type'] == 'episode' and progress_percent < jellypy.CONFIG.TV_WATCHED_PERCENT)) elif notify_action == 'on_resume': evaluated = jellypy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99 @@ -218,7 +206,8 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None): else: evaluated = False - logger.debug("Tautulli NotificationHandler :: Global notification conditions evaluated to '{}'.".format(evaluated)) + logger.debug( + "Tautulli NotificationHandler :: Global notification conditions evaluated to '{}'.".format(evaluated)) # Recently Added notifications elif timeline_data: @@ -272,7 +261,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None): if not parameter or not operator or not values: evaluated = True evaluated_conditions.append(evaluated) - logger.debug("Tautulli NotificationHandler :: {%s} Blank condition > %s" % (i+1, evaluated)) + logger.debug("Tautulli NotificationHandler :: {%s} Blank condition > %s" % (i + 1, evaluated)) continue # Make sure the condition values is in a list @@ -291,8 +280,9 @@ def notify_custom_conditions(notifier_id=None, parameters=None): values = [helpers.cast_to_float(v) for v in values] except ValueError as e: - logger.error("Tautulli NotificationHandler :: {%s} Unable to cast condition '%s', values '%s', to type '%s'." - % (i+1, parameter, values, parameter_type)) + logger.error( + "Tautulli NotificationHandler :: {%s} Unable to cast condition '%s', values '%s', to type '%s'." + % (i + 1, parameter, values, parameter_type)) return False # Cast the parameter value to the correct type @@ -307,8 +297,9 @@ def notify_custom_conditions(notifier_id=None, parameters=None): parameter_value = helpers.cast_to_float(parameter_value) except ValueError as e: - logger.error("Tautulli NotificationHandler :: {%s} Unable to cast parameter '%s', value '%s', to type '%s'." - % (i+1, parameter, parameter_value, parameter_type)) + logger.error( + "Tautulli NotificationHandler :: {%s} Unable to cast parameter '%s', value '%s', to type '%s'." + % (i + 1, parameter, parameter_value, parameter_type)) return False # Check each condition @@ -339,11 +330,12 @@ def notify_custom_conditions(notifier_id=None, parameters=None): else: evaluated = None logger.warn("Tautulli NotificationHandler :: {%s} Invalid condition operator '%s' > %s." - % (i+1, operator, evaluated)) + % (i + 1, operator, evaluated)) evaluated_conditions.append(evaluated) logger.debug("Tautulli NotificationHandler :: {%s} %s | %s | %s > '%s' > %s" - % (i+1, parameter, operator, ' or '.join(["'%s'" % v for v in values]), parameter_value, evaluated)) + % (i + 1, parameter, operator, ' or '.join(["'%s'" % v for v in values]), parameter_value, + evaluated)) if logic_groups: # Format and evaluate the logic string @@ -357,7 +349,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None): else: evaluated_logic = all(evaluated_conditions[1:]) logger.debug("Tautulli NotificationHandler :: Condition logic [blank]: %s > %s" - % (' and '.join(['{%s}' % (i+1) for i in range(len(custom_conditions))]), evaluated_logic)) + % (' and '.join(['{%s}' % (i + 1) for i in range(len(custom_conditions))]), evaluated_logic)) logger.debug("Tautulli NotificationHandler :: Custom conditions evaluated to '{}'. Conditions: {}.".format( evaluated_logic, evaluated_conditions[1:])) @@ -457,7 +449,6 @@ def get_notify_state_enabled(session, notify_action, notified=True): def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None): - if notifier and notify_action: monitor_db = database.MonitorDatabase() @@ -571,7 +562,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m total_bandwidth = lan_bandwidth + wan_bandwidth # Generate a combined transcode decision value - if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode': + if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', + '') == 'transcode': transcode_decision = 'Transcode' elif session.get('stream_video_decision', '') == 'copy' or session.get('stream_audio_decision', '') == 'copy': transcode_decision = 'Direct Stream' @@ -597,7 +589,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m else: plex_web_rating_key = notify_params['rating_key'] - notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format( + notify_params[ + 'plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format( web_url=jellypy.CONFIG.PMS_WEB_URL, pms_identifier=jellypy.CONFIG.PMS_IDENTIFIER, rating_key=plex_web_rating_key) @@ -621,7 +614,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id'] if 'thetvdb://' in notify_params['guid'] or notify_params['thetvdb_id']: - notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0] + notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or \ + notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0] notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id'] notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show' @@ -632,12 +626,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m if 'themoviedb://' in notify_params['guid'] or notify_params['themoviedb_id']: if notify_params['media_type'] == 'movie': - notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('?')[0] + notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or \ + notify_params['guid'].split('themoviedb://')[1].split('?')[0] notify_params['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + notify_params['themoviedb_id'] - notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?type=movie' + notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params[ + 'themoviedb_id'] + '?type=movie' elif notify_params['media_type'] in ('show', 'season', 'episode'): - notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0] + notify_params['themoviedb_id'] = notify_params['themoviedb_id'] or \ + notify_params['guid'].split('themoviedb://')[1].split('/')[0].split('?')[0] notify_params['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + notify_params['themoviedb_id'] notify_params['trakt_url'] = 'https://trakt.tv/search/tmdb/' + notify_params['themoviedb_id'] + '?type=show' @@ -710,7 +707,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m if tvmaze_info.get('thetvdb_id'): notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id']) - notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(notify_params['thetvdb_id']) + '?type=show' + notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str( + notify_params['thetvdb_id']) + '?type=show' if tvmaze_info.get('imdb_id'): notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id'] notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id'] @@ -779,7 +777,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m notify_params.update(poster_info) if ((manual_trigger or jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT) - and notify_params['media_type'] in ('show', 'artist')): + and notify_params['media_type'] in ('show', 'artist')): show_name = notify_params['title'] episode_name = '' artist_name = notify_params['title'] @@ -999,15 +997,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m 'track_count': grandchild_count, 'year': notify_params['year'], 'release_date': arrow.get(notify_params['originally_available_at']).format(date_format) - if notify_params['originally_available_at'] else '', + if notify_params['originally_available_at'] else '', 'air_date': arrow.get(notify_params['originally_available_at']).format(date_format) - if notify_params['originally_available_at'] else '', + if notify_params['originally_available_at'] else '', 'added_date': arrow.get(notify_params['added_at']).format(date_format) - if notify_params['added_at'] else '', + if notify_params['added_at'] else '', 'updated_date': arrow.get(notify_params['updated_at']).format(date_format) - if notify_params['updated_at'] else '', + if notify_params['updated_at'] else '', 'last_viewed_date': arrow.get(notify_params['last_viewed_at']).format(date_format) - if notify_params['last_viewed_at'] else '', + if notify_params['last_viewed_at'] else '', 'studio': notify_params['studio'], 'content_rating': notify_params['content_rating'], 'directors': ', '.join(notify_params['directors']), @@ -1019,7 +1017,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m 'summary': notify_params['summary'], 'tagline': notify_params['tagline'], 'rating': rating, - 'critic_rating': critic_rating, + 'critic_rating': critic_rating, 'audience_rating': audience_rating, 'user_rating': notify_params['user_rating'], 'duration': duration, @@ -1089,7 +1087,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m 'parent_thumb': notify_params['parent_thumb'], 'grandparent_thumb': notify_params['grandparent_thumb'], 'poster_thumb': poster_thumb - } + } return available_params @@ -1147,7 +1145,7 @@ def build_server_notify_params(notify_action=None, **kwargs): 'update_version': pms_download_info['version'], 'update_url': pms_download_info['download_url'], 'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format) - if pms_download_info['release_date'] else '', + if pms_download_info['release_date'] else '', 'update_channel': 'Beta' if update_channel == 'beta' else 'Public', 'update_platform': pms_download_info['platform'], 'update_distro': pms_download_info['distro'], @@ -1164,7 +1162,7 @@ def build_server_notify_params(notify_action=None, **kwargs): 'tautulli_update_commit': kwargs.pop('plexpy_update_commit', ''), 'tautulli_update_behind': kwargs.pop('plexpy_update_behind', ''), 'tautulli_update_changelog': plexpy_download_info['body'] - } + } return available_params @@ -1189,8 +1187,8 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None, media_type = parameters.get('media_type') all_tags = r'.*?|' \ - '.*?|.*?|.*?|' \ - '.*?|.*?|.*?' + '.*?|.*?|.*?|' \ + '.*?|.*?|.*?' # Check for exclusion tags if media_type == 'movie': @@ -1200,7 +1198,8 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None, elif media_type == 'season': pattern = re.compile(all_tags.replace('.*?', '|'), re.IGNORECASE | re.DOTALL) elif media_type == 'episode': - pattern = re.compile(all_tags.replace('.*?', '|'), re.IGNORECASE | re.DOTALL) + pattern = re.compile(all_tags.replace('.*?', '|'), + re.IGNORECASE | re.DOTALL) elif media_type == 'artist': pattern = re.compile(all_tags.replace('.*?', '|'), re.IGNORECASE | re.DOTALL) elif media_type == 'album': @@ -1224,10 +1223,12 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None, try: script_args = [str_formatter(arg) for arg in helpers.split_args(subject)] except LookupError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e) script_args = [] except Exception as e: - logger.exception("Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e) + logger.exception( + "Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e) script_args = [] elif agent_id == 25: @@ -1235,51 +1236,61 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None, try: subject = json.loads(subject) except ValueError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse custom webhook json header data: %s. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse custom webhook json header data: %s. Using fallback." % e) subject = '' if subject: try: subject = json.dumps(helpers.traverse_map(subject, str_formatter)) except LookupError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in webhook header data. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse parameter %s in webhook header data. Using fallback." % e) subject = '' except Exception as e: - logger.exception("Tautulli NotificationHandler :: Unable to parse custom webhook header data: %s. Using fallback." % e) + logger.exception( + "Tautulli NotificationHandler :: Unable to parse custom webhook header data: %s. Using fallback." % e) subject = '' if body: try: body = json.loads(body) except ValueError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse custom webhook json body data: %s. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse custom webhook json body data: %s. Using fallback." % e) body = '' if body: try: body = json.dumps(helpers.traverse_map(body, str_formatter)) except LookupError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in webhook body data. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse parameter %s in webhook body data. Using fallback." % e) body = '' except Exception as e: - logger.exception("Tautulli NotificationHandler :: Unable to parse custom webhook body data: %s. Using fallback." % e) + logger.exception( + "Tautulli NotificationHandler :: Unable to parse custom webhook body data: %s. Using fallback." % e) body = '' else: try: subject = str_formatter(subject) except LookupError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e) subject = str(default_subject).format(**parameters) except Exception as e: - logger.exception("Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e) + logger.exception( + "Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e) subject = str(default_subject).format(**parameters) try: body = str_formatter(body) except LookupError as e: - logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e) + logger.error( + "Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e) body = str(default_body).format(**parameters) except Exception as e: - logger.exception("Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e) + logger.exception( + "Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e) body = str(default_body).format(**parameters) return subject, body, script_args @@ -1325,7 +1336,7 @@ def format_group_index(group_keys): num = [] num00 = [] - for k, g in groupby(enumerate(group_keys), lambda i_x: i_x[0]-i_x[1]): + for k, g in groupby(enumerate(group_keys), lambda i_x: i_x[0] - i_x[1]): group = list(map(itemgetter(1), g)) g_min, g_max = min(group), max(group) @@ -1499,14 +1510,16 @@ def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, title=No 'WHERE rating_key = ?' tvmaze_info = db.select_single(query, args=[rating_key]) except Exception as e: - logger.warn("Tautulli NotificationHandler :: Unable to execute database query for lookup_tvmaze_by_tvdb_id: %s." % e) + logger.warn( + "Tautulli NotificationHandler :: Unable to execute database query for lookup_tvmaze_by_tvdb_id: %s." % e) return {} if not tvmaze_info: tvmaze_info = {} if thetvdb_id: - logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for thetvdb_id '{}'.".format(thetvdb_id)) + logger.debug( + "Tautulli NotificationHandler :: Looking up TVmaze info for thetvdb_id '{}'.".format(thetvdb_id)) elif imdb_id: logger.debug("Tautulli NotificationHandler :: Looking up TVmaze info for imdb_id '{}'.".format(imdb_id)) else: @@ -1559,18 +1572,23 @@ def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, titl 'WHERE rating_key = ?' themoviedb_info = db.select_single(query, args=[rating_key]) except Exception as e: - logger.warn("Tautulli NotificationHandler :: Unable to execute database query for lookup_themoviedb_by_imdb_id: %s." % e) + logger.warn( + "Tautulli NotificationHandler :: Unable to execute database query for lookup_themoviedb_by_imdb_id: %s." % e) return {} if not themoviedb_info: themoviedb_info = {} if thetvdb_id: - logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for thetvdb_id '{}'.".format(thetvdb_id)) + logger.debug( + "Tautulli NotificationHandler :: Looking up The Movie Database info for thetvdb_id '{}'.".format( + thetvdb_id)) elif imdb_id: - logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for imdb_id '{}'.".format(imdb_id)) + logger.debug( + "Tautulli NotificationHandler :: Looking up The Movie Database info for imdb_id '{}'.".format(imdb_id)) else: - logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year)) + logger.debug( + "Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year)) params = {'api_key': jellypy.CONFIG.THEMOVIEDB_APIKEY} @@ -1648,10 +1666,12 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None): themoviedb_json = {} - logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format(themoviedb_id)) + logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format( + themoviedb_id)) params = {'api_key': jellypy.CONFIG.THEMOVIEDB_APIKEY} - response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params) + response, err_msg, req_msg = request.request_response2( + 'https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params) if response and not err_msg: themoviedb_json = response.json() @@ -1876,7 +1896,7 @@ class CustomFormatter(Formatter): else: format_spec, auto_arg_index = self._vformat( format_spec, args, kwargs, - used_args, recursion_depth-1, + used_args, recursion_depth - 1, auto_arg_index=auto_arg_index) # format the object and append to the result diff --git a/jellypy/notifiers.py b/jellypy/notifiers.py index b477602f..2338e51d 100644 --- a/jellypy/notifiers.py +++ b/jellypy/notifiers.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str -from future.builtins import object import base64 import bleach @@ -34,8 +31,6 @@ import subprocess import sys import threading import time -from future.moves.urllib.parse import urlencode -from future.moves.urllib.parse import urlparse try: from Cryptodome.Protocol.KDF import PBKDF2 diff --git a/jellypy/plex.py b/jellypy/plex.py index fb48a9c2..e5fac724 100644 --- a/jellypy/plex.py +++ b/jellypy/plex.py @@ -16,8 +16,6 @@ # along with Tautulli. If not, see . from __future__ import unicode_literals -from future.builtins import object -from future.builtins import str from plexapi.server import PlexServer diff --git a/jellypy/plexivity_import.py b/jellypy/plexivity_import.py index 9103296b..de561d5d 100644 --- a/jellypy/plexivity_import.py +++ b/jellypy/plexivity_import.py @@ -15,26 +15,16 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str - -import arrow import sqlite3 from xml.dom import minidom -import jellypy -if jellypy.PYTHON2: - import activity_processor - import database - import helpers - import logger - import users -else: - from jellypy import activity_processor - from jellypy import database - from jellypy import helpers - from jellypy import logger - from jellypy import users +import arrow + +from jellypy import activity_processor +from jellypy import database +from jellypy import helpers +from jellypy import logger +from jellypy import users def extract_plexivity_xml(xml=None): @@ -274,7 +264,6 @@ def validate_database(database_file=None, table_name=None): def import_from_plexivity(database_file=None, table_name=None, import_ignore_interval=0): - try: connection = sqlite3.connect(database_file, timeout=20) connection.row_factory = sqlite3.Row @@ -339,7 +328,7 @@ def import_from_plexivity(database_file=None, table_name=None, import_ignore_int continue # Skip line if we don't have a ratingKey to work with - #if not row['rating_key']: + # if not row['rating_key']: # logger.error("Tautulli Importer :: Skipping record due to null ratingKey.") # continue diff --git a/jellypy/plextv.py b/jellypy/plextv.py index 85174c5a..5a39ff6a 100644 --- a/jellypy/plextv.py +++ b/jellypy/plextv.py @@ -15,31 +15,17 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import str -from future.builtins import object - -import base64 import json import jellypy -if jellypy.PYTHON2: - import common - import helpers - import http_handler - import logger - import users - import pmsconnect - import session -else: - from jellypy import common - from jellypy import helpers - from jellypy import http_handler - from jellypy import logger - from jellypy import users - from jellypy import pmsconnect - from jellypy import session + +from jellypy import common +from jellypy import helpers +from jellypy import http_handler +from jellypy import logger +from jellypy import users +from jellypy import pmsconnect +from jellypy import session def get_server_resources(return_presence=False, return_server=False, return_info=False, **kwargs): @@ -212,7 +198,8 @@ class PlexTV(object): if force: logger.debug("Tautulli PlexTV :: Forcing refresh of Plex.tv token.") devices_list = self.get_devices_list() - device_id = next((d for d in devices_list if d['device_identifier'] == jellypy.CONFIG.PMS_UUID), {}).get('device_id', None) + device_id = next((d for d in devices_list if d['device_identifier'] == jellypy.CONFIG.PMS_UUID), {}).get( + 'device_id', None) if device_id: logger.debug("Tautulli PlexTV :: Removing Tautulli from Plex.tv devices.") @@ -233,7 +220,6 @@ class PlexTV(object): logger.info("Tautulli PlexTV :: Updated Plex.tv token for Tautulli.") return token - def get_server_token(self): servers = self.get_plextv_resources(output_format='xml') server_token = '' @@ -794,7 +780,8 @@ class PlexTV(object): 'port': helpers.get_xml_attr(c, 'port'), 'uri': helpers.get_xml_attr(c, 'uri'), 'local': helpers.get_xml_attr(c, 'local'), - 'value': helpers.get_xml_attr(c, 'address') + ':' + helpers.get_xml_attr(c, 'port'), + 'value': helpers.get_xml_attr(c, 'address') + ':' + helpers.get_xml_attr(c, + 'port'), 'is_cloud': is_cloud } clean_servers.append(server) @@ -826,15 +813,17 @@ class PlexTV(object): # Get the updates for the platform pms_platform = common.PMS_PLATFORM_NAME_OVERRIDES.get(jellypy.CONFIG.PMS_PLATFORM, jellypy.CONFIG.PMS_PLATFORM) platform_downloads = available_downloads.get('computer').get(pms_platform) or \ - available_downloads.get('nas').get(pms_platform) + available_downloads.get('nas').get(pms_platform) if not platform_downloads: logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Could not match server platform: %s." % pms_platform) return {} - v_old = helpers.cast_to_int("".join(v.zfill(4) for v in jellypy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4])) - v_new = helpers.cast_to_int("".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4])) + v_old = helpers.cast_to_int( + "".join(v.zfill(4) for v in jellypy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4])) + v_new = helpers.cast_to_int( + "".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4])) if not v_old: logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid current server version: %s." @@ -983,7 +972,7 @@ class PlexTV(object): "latitude": latitude, "longitude": longitude, "continent": None, # keep for backwards compatibility with GeoLite2 - "accuracy": None # keep for backwards compatibility with GeoLite2 + "accuracy": None # keep for backwards compatibility with GeoLite2 } return geo_info diff --git a/jellypy/plexwatch_import.py b/jellypy/plexwatch_import.py index edd1aeae..79139fdb 100644 --- a/jellypy/plexwatch_import.py +++ b/jellypy/plexwatch_import.py @@ -15,25 +15,14 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str - import sqlite3 from xml.dom import minidom -import jellypy -if jellypy.PYTHON2: - import activity_processor - import database - import helpers - import logger - import users -else: - from jellypy import activity_processor - from jellypy import database - from jellypy import helpers - from jellypy import logger - from jellypy import users +from jellypy import activity_processor +from jellypy import database +from jellypy import helpers +from jellypy import logger +from jellypy import users def extract_plexwatch_xml(xml=None): @@ -265,7 +254,6 @@ def validate_database(database_file=None, table_name=None): def import_from_plexwatch(database_file=None, table_name=None, import_ignore_interval=0): - try: connection = sqlite3.connect(database_file, timeout=20) connection.row_factory = sqlite3.Row diff --git a/jellypy/pmsconnect.py b/jellypy/pmsconnect.py index 6ad95a85..5edb8a8d 100644 --- a/jellypy/pmsconnect.py +++ b/jellypy/pmsconnect.py @@ -15,38 +15,21 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import str -from future.builtins import object - import json import os -import time -from future.moves.urllib.parse import quote, quote_plus, urlencode +from urllib.parse import quote_plus, quote, urlencode from xml.dom.minidom import Node import jellypy -if jellypy.PYTHON2: - import activity_processor - import common - import helpers - import http_handler - import libraries - import logger - import plextv - import session - import users -else: - from jellypy import activity_processor - from jellypy import common - from jellypy import helpers - from jellypy import http_handler - from jellypy import libraries - from jellypy import logger - from jellypy import plextv - from jellypy import session - from jellypy import users +from jellypy import activity_processor +from jellypy import common +from jellypy import helpers +from jellypy import http_handler +from jellypy import libraries +from jellypy import logger +from jellypy import plextv +from jellypy import session +from jellypy import users def get_server_friendly_name(): @@ -217,7 +200,8 @@ class PmsConnect(object): Output: array """ - uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (section_id, start, count) + uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % ( + section_id, start, count) request = self.request_handler.make_request(uri=uri, request_type='GET', output_format=output_format) @@ -474,7 +458,7 @@ class PmsConnect(object): return request - def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''): + def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''): """ Return processed and validated list of recently added items. @@ -889,7 +873,7 @@ class PmsConnect(object): 'collections': show_details.get('collections', []), 'guids': show_details.get('guids', []), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), - helpers.get_xml_attr(metadata_main, 'title')), + helpers.get_xml_attr(metadata_main, 'title')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') } @@ -962,7 +946,7 @@ class PmsConnect(object): 'collections': show_details.get('collections', []), 'guids': show_details.get('guids', []), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'grandparentTitle'), - helpers.get_xml_attr(metadata_main, 'title')), + helpers.get_xml_attr(metadata_main, 'title')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') } @@ -1063,7 +1047,7 @@ class PmsConnect(object): 'collections': collections, 'guids': guids, 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'), - helpers.get_xml_attr(metadata_main, 'title')), + helpers.get_xml_attr(metadata_main, 'title')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') } @@ -1117,7 +1101,7 @@ class PmsConnect(object): 'collections': album_details.get('collections', []), 'guids': album_details.get('guids', []), 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'title'), - track_artist), + track_artist), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') } @@ -1217,8 +1201,9 @@ class PmsConnect(object): 'labels': photo_album_details.get('labels', []), 'collections': photo_album_details.get('collections', []), 'guids': photo_album_details.get('guids', []), - 'full_title': '{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name, - helpers.get_xml_attr(metadata_main, 'title')), + 'full_title': '{} - {}'.format( + helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name, + helpers.get_xml_attr(metadata_main, 'title')), 'children_count': helpers.cast_to_int(helpers.get_xml_attr(metadata_main, 'leafCount')), 'live': int(helpers.get_xml_attr(metadata_main, 'live') == '1') } @@ -1385,7 +1370,8 @@ class PmsConnect(object): 'video_codec_level': helpers.get_xml_attr(stream, 'level'), 'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'), 'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'), - 'video_chroma_subsampling': helpers.get_xml_attr(stream, 'chromaSubsampling'), + 'video_chroma_subsampling': helpers.get_xml_attr(stream, + 'chromaSubsampling'), 'video_color_primaries': helpers.get_xml_attr(stream, 'colorPrimaries'), 'video_color_range': helpers.get_xml_attr(stream, 'colorRange'), 'video_color_space': helpers.get_xml_attr(stream, 'colorSpace'), @@ -1423,7 +1409,8 @@ class PmsConnect(object): 'subtitle_container': helpers.get_xml_attr(stream, 'container'), 'subtitle_format': helpers.get_xml_attr(stream, 'format'), 'subtitle_forced': int(helpers.get_xml_attr(stream, 'forced') == '1'), - 'subtitle_location': 'external' if helpers.get_xml_attr(stream, 'key') else 'embedded', + 'subtitle_location': 'external' if helpers.get_xml_attr(stream, + 'key') else 'embedded', 'subtitle_language': helpers.get_xml_attr(stream, 'language'), 'subtitle_language_code': helpers.get_xml_attr(stream, 'languageCode'), 'selected': int(helpers.get_xml_attr(stream, 'selected') == '1') @@ -1673,7 +1660,8 @@ class PmsConnect(object): platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default') player_details = {'ip_address': helpers.get_xml_attr(player_info, 'address').split('::ffff:')[-1], - 'ip_address_public': helpers.get_xml_attr(player_info, 'remotePublicAddress').split('::ffff:')[-1], + 'ip_address_public': + helpers.get_xml_attr(player_info, 'remotePublicAddress').split('::ffff:')[-1], 'device': helpers.get_xml_attr(player_info, 'device'), 'platform': platform, 'platform_name': platform_name, @@ -1681,7 +1669,8 @@ class PmsConnect(object): 'product': helpers.get_xml_attr(player_info, 'product'), 'product_version': helpers.get_xml_attr(player_info, 'version'), 'profile': helpers.get_xml_attr(player_info, 'profile'), - 'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info, 'product'), + 'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info, + 'product'), 'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier'), 'state': helpers.get_xml_attr(player_info, 'state'), 'local': int(helpers.get_xml_attr(player_info, 'local') == '1'), @@ -1731,20 +1720,27 @@ class PmsConnect(object): 'transcode_audio_channels': helpers.get_xml_attr(transcode_info, 'audioChannels'), 'transcode_audio_codec': helpers.get_xml_attr(transcode_info, 'audioCodec'), 'transcode_video_codec': helpers.get_xml_attr(transcode_info, 'videoCodec'), - 'transcode_width': helpers.get_xml_attr(transcode_info, 'width'), # Blank but keep for backwards compatibility - 'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), # Blank but keep backwards compatibility + 'transcode_width': helpers.get_xml_attr(transcode_info, 'width'), + # Blank but keep for backwards compatibility + 'transcode_height': helpers.get_xml_attr(transcode_info, 'height'), + # Blank but keep backwards compatibility 'transcode_container': helpers.get_xml_attr(transcode_info, 'container'), 'transcode_protocol': helpers.get_xml_attr(transcode_info, 'protocol'), - 'transcode_hw_requested': int(helpers.get_xml_attr(transcode_info, 'transcodeHwRequested') == '1'), + 'transcode_hw_requested': int( + helpers.get_xml_attr(transcode_info, 'transcodeHwRequested') == '1'), 'transcode_hw_decode': helpers.get_xml_attr(transcode_info, 'transcodeHwDecoding'), - 'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwDecodingTitle'), + 'transcode_hw_decode_title': helpers.get_xml_attr(transcode_info, + 'transcodeHwDecodingTitle'), 'transcode_hw_encode': helpers.get_xml_attr(transcode_info, 'transcodeHwEncoding'), - 'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info, 'transcodeHwEncodingTitle'), - 'transcode_hw_full_pipeline': int(helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1'), + 'transcode_hw_encode_title': helpers.get_xml_attr(transcode_info, + 'transcodeHwEncodingTitle'), + 'transcode_hw_full_pipeline': int( + helpers.get_xml_attr(transcode_info, 'transcodeHwFullPipeline') == '1'), 'audio_decision': helpers.get_xml_attr(transcode_info, 'audioDecision'), 'video_decision': helpers.get_xml_attr(transcode_info, 'videoDecision'), 'subtitle_decision': helpers.get_xml_attr(transcode_info, 'subtitleDecision'), - 'throttled': '1' if helpers.get_xml_attr(transcode_info, 'throttled') == '1' else '0' # Keep for backwards compatibility + 'throttled': '1' if helpers.get_xml_attr(transcode_info, 'throttled') == '1' else '0' + # Keep for backwards compatibility } else: transcode_session = False @@ -1773,8 +1769,10 @@ class PmsConnect(object): } # Check HW decoding/encoding - transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS) - transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS) + transcode_details['transcode_hw_decoding'] = int( + transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS) + transcode_details['transcode_hw_encoding'] = int( + transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS) # Determine if a synced version is being played sync_id = synced_session_data = synced_item_details = None @@ -1788,7 +1786,8 @@ class PmsConnect(object): grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey') synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'], - rating_key_filter=[rating_key, parent_rating_key, grandparent_rating_key]) + rating_key_filter=[rating_key, parent_rating_key, + grandparent_rating_key]) if synced_items: synced_item_details = synced_items[0] sync_id = synced_item_details['sync_id'] @@ -1810,9 +1809,11 @@ class PmsConnect(object): media_info_all = synced_session_data.getElementsByTagName('Media') else: media_info_all = session.getElementsByTagName('Media') - stream_media_info = next((m for m in media_info_all if helpers.get_xml_attr(m, 'selected') == '1'), media_info_all[0]) + stream_media_info = next((m for m in media_info_all if helpers.get_xml_attr(m, 'selected') == '1'), + media_info_all[0]) part_info_all = stream_media_info.getElementsByTagName('Part') - stream_media_parts_info = next((p for p in part_info_all if helpers.get_xml_attr(p, 'selected') == '1'), part_info_all[0]) + stream_media_parts_info = next((p for p in part_info_all if helpers.get_xml_attr(p, 'selected') == '1'), + part_info_all[0]) # Get the stream details video_stream_info = audio_stream_info = subtitle_stream_info = None @@ -1834,7 +1835,8 @@ class PmsConnect(object): video_id = helpers.get_xml_attr(video_stream_info, 'id') video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'), 'stream_video_bit_depth': helpers.get_xml_attr(video_stream_info, 'bitDepth'), - 'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info, 'chromaSubsampling'), + 'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info, + 'chromaSubsampling'), 'stream_video_color_primaries': helpers.get_xml_attr(video_stream_info, 'colorPrimaries'), 'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'), 'stream_video_color_space': helpers.get_xml_attr(video_stream_info, 'colorSpace'), @@ -1844,7 +1846,8 @@ class PmsConnect(object): 'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'), 'stream_video_language_code': helpers.get_xml_attr(video_stream_info, 'languageCode'), 'stream_video_scan_type': helpers.get_xml_attr(video_stream_info, 'scanType'), - 'stream_video_decision': helpers.get_xml_attr(video_stream_info, 'decision') or 'direct play' + 'stream_video_decision': helpers.get_xml_attr(video_stream_info, + 'decision') or 'direct play' } else: video_details = {'stream_video_bitrate': '', @@ -1867,10 +1870,12 @@ class PmsConnect(object): audio_details = {'stream_audio_bitrate': helpers.get_xml_attr(audio_stream_info, 'bitrate'), 'stream_audio_bitrate_mode': helpers.get_xml_attr(audio_stream_info, 'bitrateMode'), 'stream_audio_sample_rate': helpers.get_xml_attr(audio_stream_info, 'samplingRate'), - 'stream_audio_channel_layout_': helpers.get_xml_attr(audio_stream_info, 'audioChannelLayout'), + 'stream_audio_channel_layout_': helpers.get_xml_attr(audio_stream_info, + 'audioChannelLayout'), 'stream_audio_language': helpers.get_xml_attr(audio_stream_info, 'language'), 'stream_audio_language_code': helpers.get_xml_attr(audio_stream_info, 'languageCode'), - 'stream_audio_decision': helpers.get_xml_attr(audio_stream_info, 'decision') or 'direct play' + 'stream_audio_decision': helpers.get_xml_attr(audio_stream_info, + 'decision') or 'direct play' } else: audio_details = {'stream_audio_bitrate': '', @@ -1888,12 +1893,15 @@ class PmsConnect(object): subtitle_details = {'stream_subtitle_codec': helpers.get_xml_attr(subtitle_stream_info, 'codec'), 'stream_subtitle_container': helpers.get_xml_attr(subtitle_stream_info, 'container'), 'stream_subtitle_format': helpers.get_xml_attr(subtitle_stream_info, 'format'), - 'stream_subtitle_forced': int(helpers.get_xml_attr(subtitle_stream_info, 'forced') == '1'), + 'stream_subtitle_forced': int( + helpers.get_xml_attr(subtitle_stream_info, 'forced') == '1'), 'stream_subtitle_location': helpers.get_xml_attr(subtitle_stream_info, 'location'), 'stream_subtitle_language': helpers.get_xml_attr(subtitle_stream_info, 'language'), - 'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info, 'languageCode'), + 'stream_subtitle_language_code': helpers.get_xml_attr(subtitle_stream_info, + 'languageCode'), 'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision'), - 'stream_subtitle_transient': int(helpers.get_xml_attr(subtitle_stream_info, 'transient') == '1') + 'stream_subtitle_transient': int( + helpers.get_xml_attr(subtitle_stream_info, 'transient') == '1') } else: subtitle_selected = None @@ -1913,7 +1921,8 @@ class PmsConnect(object): view_offset = helpers.get_xml_attr(session, 'viewOffset') if indexes == 'sd': part_id = helpers.get_xml_attr(stream_media_parts_info, 'id') - bif_thumb = '/library/parts/{part_id}/indexes/sd/{view_offset}'.format(part_id=part_id, view_offset=view_offset) + bif_thumb = '/library/parts/{part_id}/indexes/sd/{view_offset}'.format(part_id=part_id, + view_offset=view_offset) else: bif_thumb = '' @@ -1930,14 +1939,19 @@ class PmsConnect(object): 'stream_aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'), 'stream_audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'), 'stream_audio_channels': stream_audio_channels, - 'stream_audio_channel_layout': audio_details.get('stream_audio_channel_layout_') or common.AUDIO_CHANNELS.get(stream_audio_channels, stream_audio_channels), + 'stream_audio_channel_layout': audio_details.get( + 'stream_audio_channel_layout_') or common.AUDIO_CHANNELS.get(stream_audio_channels, + stream_audio_channels), 'stream_video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'), 'stream_video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'), 'stream_video_resolution': stream_video_resolution, 'stream_video_height': helpers.get_xml_attr(stream_media_info, 'height'), 'stream_video_width': helpers.get_xml_attr(stream_media_info, 'width'), - 'stream_duration': helpers.get_xml_attr(stream_media_info, 'duration') or helpers.get_xml_attr(session, 'duration'), - 'stream_container_decision': 'direct play' if sync_id else helpers.get_xml_attr(stream_media_parts_info, 'decision').replace('directplay', 'direct play'), + 'stream_duration': helpers.get_xml_attr(stream_media_info, + 'duration') or helpers.get_xml_attr(session, + 'duration'), + 'stream_container_decision': 'direct play' if sync_id else helpers.get_xml_attr( + stream_media_parts_info, 'decision').replace('directplay', 'direct play'), 'optimized_version': int(helpers.get_xml_attr(stream_media_info, 'proxyType') == '42'), 'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'), 'synced_version': 1 if sync_id else 0, @@ -2004,7 +2018,8 @@ class PmsConnect(object): 'aspect_ratio': helpers.get_xml_attr(stream_media_info, 'aspectRatio'), 'video_codec': helpers.get_xml_attr(stream_media_info, 'videoCodec'), 'video_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(), - 'video_full_resolution': helpers.get_xml_attr(stream_media_info, 'videoResolution').lower(), + 'video_full_resolution': helpers.get_xml_attr(stream_media_info, + 'videoResolution').lower(), 'video_framerate': helpers.get_xml_attr(stream_media_info, 'videoFrameRate'), 'video_profile': helpers.get_xml_attr(stream_media_info, 'videoProfile'), 'audio_codec': helpers.get_xml_attr(stream_media_info, 'audioCodec'), @@ -2031,9 +2046,11 @@ class PmsConnect(object): # Get the media info, fallback to first item if match id is not found source_medias = metadata_details.pop('media_info', []) - source_media_details = next((m for m in source_medias if m['id'] == media_id), next((m for m in source_medias), {})) + source_media_details = next((m for m in source_medias if m['id'] == media_id), + next((m for m in source_medias), {})) source_media_parts = source_media_details.pop('parts', []) - source_media_part_details = next((p for p in source_media_parts if p['id'] == part_id), next((p for p in source_media_parts), {})) + source_media_part_details = next((p for p in source_media_parts if p['id'] == part_id), + next((p for p in source_media_parts), {})) source_media_part_streams = source_media_part_details.pop('streams', []) source_video_details = {'id': '', @@ -2080,13 +2097,16 @@ class PmsConnect(object): } if video_id: source_video_details = next((p for p in source_media_part_streams if p['id'] == video_id), - next((p for p in source_media_part_streams if p['type'] == '1'), source_video_details)) + next((p for p in source_media_part_streams if p['type'] == '1'), + source_video_details)) if audio_id: source_audio_details = next((p for p in source_media_part_streams if p['id'] == audio_id), - next((p for p in source_media_part_streams if p['type'] == '2'), source_audio_details)) + next((p for p in source_media_part_streams if p['type'] == '2'), + source_audio_details)) if subtitle_id: source_subtitle_details = next((p for p in source_media_part_streams if p['id'] == subtitle_id), - next((p for p in source_media_part_streams if p['type'] == '3'), source_subtitle_details)) + next((p for p in source_media_part_streams if p['type'] == '3'), + source_subtitle_details)) # Override the thumb for clips if media_type == 'clip' and metadata_details.get('extra_type') and metadata_details['art']: @@ -2107,7 +2127,8 @@ class PmsConnect(object): transcode_details['transcode_audio_channels'], transcode_details['transcode_audio_channels']) # Generate a combined transcode decision value - if video_details['stream_video_decision'] == 'transcode' or audio_details['stream_audio_decision'] == 'transcode': + if video_details['stream_video_decision'] == 'transcode' or audio_details[ + 'stream_audio_decision'] == 'transcode': transcode_decision = 'transcode' elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy': transcode_decision = 'copy' @@ -2198,7 +2219,8 @@ class PmsConnect(object): stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate']) source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate')) try: - quailtiy_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate) + quailtiy_bitrate = min( + b for b in common.AUDIO_QUALITY_PROFILES if stream_bitrate <= b <= source_bitrate) quality_profile = common.AUDIO_QUALITY_PROFILES[quailtiy_bitrate] except ValueError: quality_profile = 'Original' @@ -2463,7 +2485,7 @@ class PmsConnect(object): 'thumb': helpers.get_xml_attr(result, 'thumb'), 'parent_thumb': helpers.get_xml_attr(a, 'thumb'), 'duration': helpers.get_xml_attr(result, 'duration') - } + } children_results_list[media_type].append(children_output) output = {'results_count': sum(len(v) for k, v in children_results_list.items()), @@ -2630,11 +2652,13 @@ class PmsConnect(object): sort_type = '' if str(section_id).isdigit(): - library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key, output_format='xml') + library_data = self.get_library_list(str(section_id), list_type, count, sort_type, label_key, + output_format='xml') elif str(rating_key).isdigit(): library_data = self.get_metadata_children(str(rating_key), output_format='xml') else: - logger.warn("Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.") + logger.warn( + "Tautulli Pmsconnect :: get_library_children called by invalid section_id or rating_key provided.") return [] try: @@ -2735,7 +2759,8 @@ class PmsConnect(object): for library in libraries_list: section_type = library['section_type'] section_id = library['section_id'] - children_list = self.get_library_children_details(section_id=section_id, section_type=section_type, count='1') + children_list = self.get_library_children_details(section_id=section_id, section_type=section_type, + count='1') if children_list: library_stats = {'section_id': section_id, @@ -2749,34 +2774,40 @@ class PmsConnect(object): } if section_type == 'show': - parent_list = self.get_library_children_details(section_id=section_id, section_type='season', count='1') + parent_list = self.get_library_children_details(section_id=section_id, section_type='season', + count='1') if parent_list: parent_stats = {'parent_count': parent_list['library_count']} library_stats.update(parent_stats) - child_list = self.get_library_children_details(section_id=section_id, section_type='episode', count='1') + child_list = self.get_library_children_details(section_id=section_id, section_type='episode', + count='1') if child_list: child_stats = {'child_count': child_list['library_count']} library_stats.update(child_stats) if section_type == 'artist': - parent_list = self.get_library_children_details(section_id=section_id, section_type='album', count='1') + parent_list = self.get_library_children_details(section_id=section_id, section_type='album', + count='1') if parent_list: parent_stats = {'parent_count': parent_list['library_count']} library_stats.update(parent_stats) - child_list = self.get_library_children_details(section_id=section_id, section_type='track', count='1') + child_list = self.get_library_children_details(section_id=section_id, section_type='track', + count='1') if child_list: child_stats = {'child_count': child_list['library_count']} library_stats.update(child_stats) if section_type == 'photo': - parent_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1') + parent_list = self.get_library_children_details(section_id=section_id, section_type='picture', + count='1') if parent_list: parent_stats = {'parent_count': parent_list['library_count']} library_stats.update(parent_stats) - child_list = self.get_library_children_details(section_id=section_id, section_type='clip', count='1') + child_list = self.get_library_children_details(section_id=section_id, section_type='clip', + count='1') if child_list: child_stats = {'child_count': child_list['library_count']} library_stats.update(child_stats) @@ -2896,7 +2927,7 @@ class PmsConnect(object): for h in hubs: if helpers.get_xml_attr(h, 'size') == '0' or \ - helpers.get_xml_attr(h, 'type') not in search_results_list: + helpers.get_xml_attr(h, 'type') not in search_results_list: continue if h.getElementsByTagName('Video'): @@ -2971,7 +3002,8 @@ class PmsConnect(object): section_id = metadata['section_id'] library_name = metadata['library_name'] except Exception as e: - logger.warn("Tautulli Pmsconnect :: Unable to get grandparent_rating_key for get_rating_keys_list: %s." % e) + logger.warn( + "Tautulli Pmsconnect :: Unable to get grandparent_rating_key for get_rating_keys_list: %s." % e) return {} # get parent_rating_keys @@ -3035,8 +3067,8 @@ class PmsConnect(object): key = int(parent_index) if match_type == 'index' else parent_title parents.update({key: - {'rating_key': int(parent_rating_key), - 'children': children} + {'rating_key': int(parent_rating_key), + 'children': children} }) key = 0 if match_type == 'index' else title @@ -3097,7 +3129,8 @@ class PmsConnect(object): # Catch the malformed XML on certain PMX version. # XML parser helper returns empty list if there is an error parsing XML if updater_status == []: - logger.warn("Plex API updater XML is broken on the current PMS version. Please update your PMS manually.") + logger.warn( + "Plex API updater XML is broken on the current PMS version. Please update your PMS manually.") logger.info("Tautulli is unable to check for Plex updates. Disabling check for Plex updates.") # Disable check for Plex updates diff --git a/jellypy/request.py b/jellypy/request.py index 0989c0ea..3305950f 100644 --- a/jellypy/request.py +++ b/jellypy/request.py @@ -15,24 +15,16 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import str - -from bs4 import BeautifulSoup +import collections from xml.dom import minidom -import collections import requests +from bs4 import BeautifulSoup from requests.packages import urllib3 import jellypy -if jellypy.PYTHON2: - import lock - import logger -else: - from jellypy import lock - from jellypy import logger - +from jellypy import lock +from jellypy import logger # Dictionary with last request times, for rate limiting. last_requests = collections.defaultdict(int) @@ -182,7 +174,7 @@ def request_response2(url, method="get", auto_raise=True, err_msg = "Unable to connect to remote host because of a SSL error." else: err_msg = "Unable to connect to remote host because of a SSL error, " \ - "with certificate verification turned off: {}".format(e) + "with certificate verification turned off: {}".format(e) except requests.ConnectionError: err_msg = "Unable to connect to remote host. Check if the remote host is up and running." diff --git a/jellypy/session.py b/jellypy/session.py index fca5b415..d759d7ff 100644 --- a/jellypy/session.py +++ b/jellypy/session.py @@ -14,19 +14,11 @@ # # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . - -from __future__ import unicode_literals -from future.builtins import str - import cherrypy import jellypy -if jellypy.PYTHON2: - import common - import users -else: - from jellypy import common - from jellypy import users +from jellypy import common +from jellypy import users def get_session_info(): @@ -43,6 +35,7 @@ def get_session_info(): return _session + def get_session_user(): """ Returns the user_id for the current logged in session @@ -50,6 +43,7 @@ def get_session_user(): _session = get_session_info() return _session['user'] if _session['user_group'] == 'guest' and _session['user'] else None + def get_session_user_id(): """ Returns the user_id for the current logged in session @@ -80,6 +74,7 @@ def get_session_shared_libraries(): user_details = users.Users().get_details(user_id=get_session_user_id()) return tuple(str(s) for s in user_details['shared_libraries']) + def get_session_library_filters(): """ Returns a dict of library filters for the current logged in session @@ -91,6 +86,7 @@ def get_session_library_filters(): filters = users.Users().get_filters(user_id=get_session_user_id()) return filters + def get_session_library_filters_type(filters, media_type=None): """ Returns a dict of library filters for the current logged in session @@ -115,6 +111,7 @@ def get_session_library_filters_type(filters, media_type=None): return content_rating, tuple(f.lower() for f in labels) + def allow_session_user(user_id): """ Returns True or False if the user_id is allowed for the current logged in session @@ -124,6 +121,7 @@ def allow_session_user(user_id): return False return True + def allow_session_library(section_id): """ Returns True or False if the section_id is allowed for the current logged in session @@ -133,13 +131,14 @@ def allow_session_library(section_id): return False return True + def friendly_name_to_username(list_of_dicts): """ Reverts the friendly name back to the username of the current logged in session """ session_user = get_session_user() session_user_id = get_session_user_id() - + if session_user_id: for d in list_of_dicts: if 'friendly_name' in d and d['friendly_name'] != session_user: @@ -147,12 +146,13 @@ def friendly_name_to_username(list_of_dicts): return list_of_dicts + def filter_session_info(list_of_dicts, filter_key=None): """ Filters a list of dictionary items to only return the info for the current logged in session """ session_user_id = get_session_user_id() - + if not session_user_id: return list_of_dicts @@ -162,13 +162,13 @@ def filter_session_info(list_of_dicts, filter_key=None): list_of_dicts = friendly_name_to_username(list_of_dicts) if filter_key == 'user_id' and session_user_id: - return [d for d in list_of_dicts if str(d.get('user_id','')) == session_user_id] + return [d for d in list_of_dicts if str(d.get('user_id', '')) == session_user_id] elif filter_key == 'section_id' and session_library_ids: new_list_of_dicts = [] for d in list_of_dicts: - if str(d.get('section_id','')) not in session_library_ids: + if str(d.get('section_id', '')) not in session_library_ids: continue if d.get('media_type'): @@ -198,6 +198,7 @@ def filter_session_info(list_of_dicts, filter_key=None): return list_of_dicts + def mask_session_info(list_of_dicts, mask_metadata=True): """ Masks user info in a list of dictionary items to only display info for the current logged in session @@ -249,7 +250,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True): if not mask_metadata: continue - if str(d.get('section_id','')) not in session_library_ids: + if str(d.get('section_id', '')) not in session_library_ids: for k, v in metadata_to_mask.items(): if k in d: d[k] = metadata_to_mask[k] continue @@ -257,7 +258,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True): media_type = d.get('media_type') if media_type: f_content_rating, f_labels = get_session_library_filters_type(session_library_filters, - media_type=d['media_type']) + media_type=d['media_type']) d_content_rating = d.get('content_rating', '') d_labels = tuple(f.lower() for f in d.get('labels', ())) @@ -277,4 +278,4 @@ def mask_session_info(list_of_dicts, mask_metadata=True): for k, v in metadata_to_mask.items(): if k in d: d[k] = metadata_to_mask[k] - return list_of_dicts \ No newline at end of file + return list_of_dicts diff --git a/jellypy/users.py b/jellypy/users.py index af4e2b22..c6eaa321 100644 --- a/jellypy/users.py +++ b/jellypy/users.py @@ -14,34 +14,19 @@ # # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . - -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import str -from future.builtins import object -from future.moves.urllib.parse import parse_qsl +from urllib.parse import parse_qsl import httpagentparser import jellypy -if jellypy.PYTHON2: - import common - import database - import datatables - import helpers - import libraries - import logger - import plextv - import session -else: - from jellypy import common - from jellypy import database - from jellypy import datatables - from jellypy import helpers - from jellypy import libraries - from jellypy import logger - from jellypy import plextv - from jellypy import session +from jellypy import common +from jellypy import database +from jellypy import datatables +from jellypy import helpers +from jellypy import libraries +from jellypy import logger +from jellypy import plextv +from jellypy import session def refresh_users(): @@ -847,7 +832,8 @@ class Users(object): return filters_list - def set_user_login(self, user_id=None, user=None, user_group=None, ip_address=None, host=None, user_agent=None, success=0): + def set_user_login(self, user_id=None, user=None, user_group=None, ip_address=None, host=None, user_agent=None, + success=0): if user_id is None or str(user_id).isdigit(): monitor_db = database.MonitorDatabase() @@ -947,4 +933,4 @@ class Users(object): return True except Exception as e: logger.warn("Tautulli Users :: Unable to execute database query for delete_login_log: %s." % e) - return False \ No newline at end of file + return False diff --git a/jellypy/version.py b/jellypy/version.py index b2b0afe5..f3cddd1c 100644 --- a/jellypy/version.py +++ b/jellypy/version.py @@ -15,7 +15,5 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals - PLEXPY_BRANCH = "master" PLEXPY_RELEASE_VERSION = "v2.6.5" diff --git a/jellypy/versioncheck.py b/jellypy/versioncheck.py index 8040ce9f..b1989302 100644 --- a/jellypy/versioncheck.py +++ b/jellypy/versioncheck.py @@ -15,11 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import division -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import str - import json import os import platform @@ -28,20 +23,13 @@ import subprocess import tarfile import jellypy -if jellypy.PYTHON2: - import common - import helpers - import logger - import request -else: - from jellypy import common - from jellypy import helpers - from jellypy import logger - from jellypy import request +from jellypy import common +from jellypy import helpers +from jellypy import logger +from jellypy import request def runGit(args): - if jellypy.CONFIG.GIT_PATH: git_locations = ['"' + jellypy.CONFIG.GIT_PATH + '"'] else: @@ -57,7 +45,8 @@ def runGit(args): try: logger.debug('Trying to execute: "' + cmd + '" with shell in ' + jellypy.PROG_DIR) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=jellypy.PROG_DIR) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, + cwd=jellypy.PROG_DIR) output, err = p.communicate() output = output.strip().decode() @@ -79,7 +68,6 @@ def runGit(args): def get_version(): - if jellypy.FROZEN and common.PLATFORM == 'Windows': jellypy.INSTALL_TYPE = 'windows' current_version, current_branch = get_version_from_file() @@ -270,9 +258,9 @@ def check_github(scheduler=False, notify=False, use_cache=False): if notify: jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', - 'plexpy_download_info': release, - 'plexpy_update_commit': jellypy.LATEST_VERSION, - 'plexpy_update_behind': jellypy.COMMITS_BEHIND}) + 'plexpy_download_info': release, + 'plexpy_update_commit': jellypy.LATEST_VERSION, + 'plexpy_update_behind': jellypy.COMMITS_BEHIND}) if jellypy.PYTHON2: logger.warn('Tautulli is running using Python 2. Unable to run automatic update.') diff --git a/jellypy/web_socket.py b/jellypy/web_socket.py index b0372ec5..bf9c2bc1 100644 --- a/jellypy/web_socket.py +++ b/jellypy/web_socket.py @@ -17,9 +17,6 @@ # Mostly borrowed from https://github.com/trakt/Plex-Trakt-Scrobbler -from __future__ import unicode_literals -from future.builtins import str - import json import ssl import threading @@ -29,19 +26,11 @@ import certifi import websocket import jellypy -if jellypy.PYTHON2: - import activity_handler - import activity_pinger - import activity_processor - import database - import logger -else: - from jellypy import activity_handler - from jellypy import activity_pinger - from jellypy import activity_processor - from jellypy import database - from jellypy import logger - +from jellypy import activity_handler +from jellypy import activity_pinger +from jellypy import activity_processor +from jellypy import database +from jellypy import logger name = 'websocket' opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) diff --git a/jellypy/webauth.py b/jellypy/webauth.py index 1e5a8df7..3c03e9d0 100644 --- a/jellypy/webauth.py +++ b/jellypy/webauth.py @@ -20,28 +20,20 @@ # Form based authentication for CherryPy. Requires the # Session tool to be loaded. -from future.builtins import object from datetime import datetime, timedelta -from future.moves.urllib.parse import quote, unquote +from urllib.parse import quote, unquote import cherrypy -from hashing_passwords import check_hash import jwt +from hashing_passwords import check_hash import jellypy -if jellypy.PYTHON2: - import logger - from database import MonitorDatabase - from helpers import timestamp - from users import Users, refresh_users - from plextv import PlexTV -else: - from jellypy import logger - from jellypy.database import MonitorDatabase - from jellypy.helpers import timestamp - from jellypy.users import Users, refresh_users - from jellypy.plextv import PlexTV +from jellypy import logger +from jellypy.database import MonitorDatabase +from jellypy.helpers import timestamp +from jellypy.users import Users, refresh_users +from jellypy.plextv import PlexTV # Monkey patch SameSite support into cookies. # https://stackoverflow.com/a/50813092 @@ -199,6 +191,7 @@ def check_auth(*args, **kwargs): def requireAuth(*conditions): """A decorator that appends conditions to the auth.require config variable.""" + def decorate(f): if not hasattr(f, '_cp_config'): f._cp_config = dict() @@ -206,6 +199,7 @@ def requireAuth(*conditions): f._cp_config['auth.require'] = [] f._cp_config['auth.require'].extend(conditions) return f + return decorate @@ -228,11 +222,13 @@ def name_is(user_name): def any_of(*conditions): """Returns True if any of the conditions match""" + def check(): for c in conditions: if c(): return True return False + return check @@ -240,11 +236,13 @@ def any_of(*conditions): # needed if you want to use it inside of an any_of(...) condition def all_of(*conditions): """Returns True if all of the conditions match""" + def check(): for c in conditions: if not c(): return False return True + return check diff --git a/jellypy/webserve.py b/jellypy/webserve.py index cb8324b8..a85e1953 100644 --- a/jellypy/webserve.py +++ b/jellypy/webserve.py @@ -15,14 +15,8 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals -from future.builtins import next -from future.builtins import object -from future.builtins import str -from backports import csv - -from io import open, BytesIO import base64 +import csv import json import linecache import os @@ -30,88 +24,52 @@ import shutil import sys import threading import zipfile -from future.moves.urllib.parse import urlencode +from io import open, BytesIO import cherrypy +import mako.exceptions +import mako.template +import websocket +from cherrypy import NotFound from cherrypy.lib.static import serve_file, serve_fileobj, serve_download -from cherrypy._cperror import NotFound - from hashing_passwords import make_hash from mako.lookup import TemplateLookup -import mako.template -import mako.exceptions - -import websocket import jellypy -if jellypy.PYTHON2: - import activity_pinger - import common - import config - import database - import datafactory - import exporter - import graphs - import helpers - import http_handler - import libraries - import log_reader - import logger - import newsletter_handler - import newsletters - import mobile_app - import notification_handler - import notifiers - import plextv - import plexivity_import - import plexwatch_import - import pmsconnect - import users - import versioncheck - import web_socket - import webstart - from api2 import API2 - from helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out - from session import get_session_info, get_session_user_id, allow_session_user, allow_session_library - from webauth import AuthController, requireAuth, member_of, check_auth - if common.PLATFORM == 'Windows': - import windows - elif common.PLATFORM == 'Darwin': - import macos -else: - from jellypy import activity_pinger - from jellypy import common - from jellypy import config - from jellypy import database - from jellypy import datafactory - from jellypy import exporter - from jellypy import graphs - from jellypy import helpers - from jellypy import http_handler - from jellypy import libraries - from jellypy import log_reader - from jellypy import logger - from jellypy import newsletter_handler - from jellypy import newsletters - from jellypy import mobile_app - from jellypy import notification_handler - from jellypy import notifiers - from jellypy import plextv - from jellypy import plexivity_import - from jellypy import plexwatch_import - from jellypy import pmsconnect - from jellypy import users - from jellypy import versioncheck - from jellypy import web_socket - from jellypy import webstart - from jellypy.api2 import API2 - from jellypy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out - from jellypy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library - from jellypy.webauth import AuthController, requireAuth, member_of, check_auth - if common.PLATFORM == 'Windows': - from jellypy import windows - elif common.PLATFORM == 'Darwin': - from jellypy import macos +from jellypy import activity_pinger +from jellypy import common +from jellypy import config +from jellypy import database +from jellypy import datafactory +from jellypy import exporter +from jellypy import graphs +from jellypy import helpers +from jellypy import http_handler +from jellypy import libraries +from jellypy import log_reader +from jellypy import logger +from jellypy import newsletter_handler +from jellypy import newsletters +from jellypy import mobile_app +from jellypy import notification_handler +from jellypy import notifiers +from jellypy import plextv +from jellypy import plexivity_import +from jellypy import plexwatch_import +from jellypy import pmsconnect +from jellypy import users +from jellypy import versioncheck +from jellypy import web_socket +from jellypy import webstart +from jellypy.api2 import API2 +from jellypy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out +from jellypy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library +from jellypy.webauth import AuthController, requireAuth, member_of, check_auth + +if common.PLATFORM == 'Windows': + from jellypy import windows +elif common.PLATFORM == 'Darwin': + from jellypy import macos def serve_template(templatename, **kwargs): @@ -181,7 +139,6 @@ class BaseRedirect(object): class WebInterface(object): - auth = AuthController() def __init__(self): @@ -195,7 +152,6 @@ class WebInterface(object): else: raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT + "welcome") - ##### Welcome ##### @cherrypy.expose @@ -265,7 +221,6 @@ class WebInterface(object): if servers_list: return servers_list - ##### Home ##### @cherrypy.expose @@ -455,7 +410,6 @@ class WebInterface(object): else: return {'result': 'error', 'message': 'Flush recently added failed.'} - ##### Libraries ##### @cherrypy.expose @@ -741,7 +695,8 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="library_recently_added.html", data=result['recently_added'], title="Recently Added") + return serve_template(templatename="library_recently_added.html", data=result['recently_added'], + title="Recently Added") else: logger.warn("Unable to retrieve data for library_recently_added.") return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added") @@ -1170,7 +1125,7 @@ class WebInterface(object): result = library_data.undelete(section_id=section_id, section_name=section_name) if result: if section_id: - msg ='section_id %s' % section_id + msg = 'section_id %s' % section_id elif section_name: msg = 'section_name %s' % section_name return {'result': 'success', 'message': 'Re-added library with %s.' % msg} @@ -1356,7 +1311,8 @@ class WebInterface(object): result = None status_message = 'An error occured.' - return serve_template(templatename="edit_user.html", title="Edit User", data=result, status_message=status_message) + return serve_template(templatename="edit_user.html", title="Edit User", data=result, + status_message=status_message) @cherrypy.expose @requireAuth(member_of("admin")) @@ -1801,13 +1757,12 @@ class WebInterface(object): result = user_data.undelete(user_id=user_id, username=username) if result: if user_id: - msg ='user_id %s' % user_id + msg = 'user_id %s' % user_id elif username: msg = 'username %s' % username return {'result': 'success', 'message': 'Re-added user with %s.' % msg} return {'result': 'error', 'message': 'Unable to re-add user. Invalid user_id or username.'} - ##### History ##### @cherrypy.expose @@ -2093,7 +2048,6 @@ class WebInterface(object): else: return {'result': 'error', 'message': 'No row ids received.'} - ##### Graphs ##### @cherrypy.expose @@ -2570,7 +2524,8 @@ class WebInterface(object): @cherrypy.tools.json_out() @requireAuth() @addtoapi() - def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None, **kwargs): + def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays', user_id=None, grouping=None, + **kwargs): """ Get graph data by stream type by top 10 platforms. ``` @@ -2617,7 +2572,6 @@ class WebInterface(object): return serve_template(templatename="history_table_modal.html", title="History Data", data=kwargs) - ##### Sync ##### @cherrypy.expose @@ -2673,7 +2627,6 @@ class WebInterface(object): else: return {'result': 'error', 'message': 'Missing client ID and sync ID.'} - ##### Logs ##### @cherrypy.expose @requireAuth(member_of("admin")) @@ -3026,9 +2979,9 @@ class WebInterface(object): def log_js_errors(self, page, message, file, line, **kwargs): """ Logs javascript errors from the web interface. """ logger.error("WebUI :: /%s : %s. (%s:%s)" % (page.rpartition('/')[-1], - message, - file.rpartition('/')[-1].partition('?')[0], - line)) + message, + file.rpartition('/')[-1].partition('?')[0], + line)) return "js error logged." @cherrypy.expose @@ -3047,7 +3000,6 @@ class WebInterface(object): except IOError as e: return "Log file not found." - ##### Settings ##### @cherrypy.expose @@ -3525,9 +3477,9 @@ class WebInterface(object): result = notifiers.get_notifier_config(notifier_id=notifier_id, mask_passwords=True) parameters = [ - {'name': param['name'], 'type': param['type'], 'value': param['value']} - for category in common.NOTIFICATION_PARAMETERS for param in category['parameters'] - ] + {'name': param['name'], 'type': param['type'], 'value': param['value']} + for category in common.NOTIFICATION_PARAMETERS for param in category['parameters'] + ] return serve_template(templatename="notifier_config.html", notifier=result, parameters=parameters) @@ -3646,7 +3598,8 @@ class WebInterface(object): @cherrypy.expose @cherrypy.tools.json_out() @requireAuth(member_of("admin")) - def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='', **kwargs): + def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='', + **kwargs): """ Send a notification using Tautulli. ``` @@ -3713,7 +3666,8 @@ class WebInterface(object): redirect_uri=redirect_uri) if url: - return {'result': 'success', 'msg': 'Confirm Authorization. Check pop-up blocker if no response.', 'url': url} + return {'result': 'success', 'msg': 'Confirm Authorization. Check pop-up blocker if no response.', + 'url': url} else: return {'result': 'error', 'msg': 'Failed to retrieve authorization url.'} @@ -3727,8 +3681,8 @@ class WebInterface(object): if access_token: return "Facebook authorization successful. Tautulli can send notification to Facebook. " \ - "Your Facebook access token is:" \ - "
{0}
You may close this page.".format(access_token) + "Your Facebook access token is:" \ + "
{0}
You may close this page.".format(access_token) else: return "Failed to request authorization from Facebook. Check the Tautulli logs for details.
You may close this page." @@ -3807,7 +3761,6 @@ class WebInterface(object): else: return {'result': 'error', 'message': 'Device not registered.'} - @cherrypy.expose @requireAuth(member_of("admin")) def get_mobile_device_config_modal(self, mobile_device_id=None, **kwargs): @@ -4240,7 +4193,8 @@ class WebInterface(object): @requireAuth(member_of("admin")) def generate_api_key(self, device=None, **kwargs): apikey = '' - while not apikey or apikey == jellypy.CONFIG.API_KEY or mobile_app.get_mobile_device_by_token(device_token=apikey): + while not apikey or apikey == jellypy.CONFIG.API_KEY or mobile_app.get_mobile_device_by_token( + device_token=apikey): apikey = jellypy.generate_uuid() logger.info("New API key generated.") @@ -4739,7 +4693,6 @@ class WebInterface(object): else: return "Plex log folder not set in the settings." - if log_file and os.path.isfile(log_file_path): return serve_download(log_file_path, name=log_file) else: @@ -4847,7 +4800,6 @@ class WebInterface(object): else: return {'result': 'error', 'message': 'Failed to delete lookup info.'} - ##### Search ##### @cherrypy.expose @@ -4915,7 +4867,6 @@ class WebInterface(object): logger.warn("Unable to retrieve data for get_search_results_children.") return serve_template(templatename="info_search_results_list.html", data=None, title="Search Result List") - ##### Update Metadata ##### @cherrypy.expose @@ -5304,7 +5255,8 @@ class WebInterface(object): media_type = kwargs['type'] pms_connect = pmsconnect.PmsConnect() - result = pms_connect.get_recently_added_details(start=start, count=count, media_type=media_type, section_id=section_id) + result = pms_connect.get_recently_added_details(start=start, count=count, media_type=media_type, + section_id=section_id) if result: return result @@ -6411,7 +6363,7 @@ class WebInterface(object): subject=subject, body=body, message=message, - **kwargs) + **kwargs) return {'result': 'success', 'message': 'Newsletter queued.'} else: logger.debug("Unable to send %snewsletter, invalid newsletter_id %s." % (test, newsletter_id)) @@ -6897,7 +6849,6 @@ class WebInterface(object): else: return {'result': 'error', 'message': 'Failed to delete export.'} - @cherrypy.expose @requireAuth(member_of("admin")) def exporter_docs(self, **kwargs): diff --git a/jellypy/webstart.py b/jellypy/webstart.py index 783fa4a1..79d896ff 100644 --- a/jellypy/webstart.py +++ b/jellypy/webstart.py @@ -21,16 +21,10 @@ import sys import cherrypy import jellypy -if jellypy.PYTHON2: - import logger - import webauth - from helpers import create_https_certificates - from webserve import WebInterface, BaseRedirect -else: - from jellypy import logger - from jellypy import webauth - from jellypy.helpers import create_https_certificates - from jellypy.webserve import WebInterface, BaseRedirect +from jellypy import logger +from jellypy import webauth +from jellypy.helpers import create_https_certificates +from jellypy.webserve import WebInterface, BaseRedirect def start(): @@ -64,7 +58,6 @@ def restart(): def initialize(options): - # HTTPS stuff stolen from sickbeard enable_https = options['enable_https'] https_cert = options['https_cert'] @@ -225,7 +218,7 @@ def initialize(options): 'tools.sessions.on': False, 'tools.auth.on': False }, - #'/pms_image_proxy': { + # '/pms_image_proxy': { # 'tools.staticdir.on': True, # 'tools.staticdir.dir': os.path.join(jellypy.CONFIG.CACHE_DIR, 'images'), # 'tools.caching.on': True, @@ -235,10 +228,11 @@ def initialize(options): # 'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days # 'tools.auth.on': False, # 'tools.sessions.on': False - #}, + # }, '/favicon.ico': { 'tools.staticfile.on': True, - 'tools.staticfile.filename': os.path.abspath(os.path.join(jellypy.PROG_DIR, 'data/interfaces/default/images/favicon/favicon.ico')), + 'tools.staticfile.filename': os.path.abspath( + os.path.join(jellypy.PROG_DIR, 'data/interfaces/default/images/favicon/favicon.ico')), 'tools.caching.on': True, 'tools.caching.force': True, 'tools.caching.delay': 0, @@ -256,7 +250,7 @@ def initialize(options): try: logger.info("Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol, options['http_host'], options['http_port'], options['http_root']) - #cherrypy.process.servers.check_port(str(options['http_host']), options['http_port']) + # cherrypy.process.servers.check_port(str(options['http_host']), options['http_port']) if not jellypy.DEV: cherrypy.server.start() else: