From 1df28243c3f2beb7a1d879b980cbee3fc8e5d1b0 Mon Sep 17 00:00:00 2001
From: Giovanni Harting <539@idlegandalf.com>
Date: Fri, 5 Feb 2021 17:29:23 +0100
Subject: [PATCH] More rename, more -python2
---
jellypy/__init__.py | 16 -
jellypy/activity_handler.py | 83 ++-
jellypy/activity_pinger.py | 113 ++---
jellypy/activity_processor.py | 49 +-
jellypy/api2.py | 61 +--
jellypy/classes.py | 11 +-
jellypy/common.py | 862 +++++++++++++++++++++-----------
jellypy/config.py | 19 +-
jellypy/database.py | 21 +-
jellypy/datafactory.py | 108 ++--
jellypy/datatables.py | 17 +-
jellypy/exceptions.py | 4 +-
jellypy/exporter.py | 30 +-
jellypy/graphs.py | 60 ++-
jellypy/helpers.py | 100 ++--
jellypy/http_handler.py | 17 +-
jellypy/libraries.py | 152 +++---
jellypy/lock.py | 14 +-
jellypy/log_reader.py | 16 +-
jellypy/logger.py | 46 +-
jellypy/macos.py | 15 +-
jellypy/mobile_app.py | 23 +-
jellypy/newsletter_handler.py | 28 +-
jellypy/newsletters.py | 68 ++-
jellypy/notification_handler.py | 222 ++++----
jellypy/notifiers.py | 5 -
jellypy/plex.py | 2 -
jellypy/plexivity_import.py | 27 +-
jellypy/plextv.py | 47 +-
jellypy/plexwatch_import.py | 22 +-
jellypy/pmsconnect.py | 203 ++++----
jellypy/request.py | 18 +-
jellypy/session.py | 35 +-
jellypy/users.py | 38 +-
jellypy/version.py | 2 -
jellypy/versioncheck.py | 30 +-
jellypy/web_socket.py | 21 +-
jellypy/webauth.py | 28 +-
jellypy/webserve.py | 179 +++----
jellypy/webstart.py | 24 +-
40 files changed, 1424 insertions(+), 1412 deletions(-)
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: