mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-07 13:41:15 -07:00
Run futurize --stage1
This commit is contained in:
parent
221be380ee
commit
ab6196589b
36 changed files with 736 additions and 497 deletions
|
@ -17,7 +17,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from builtins import str
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
|
@ -13,13 +13,18 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import range
|
||||||
|
|
||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
from Queue import Queue
|
import queue
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import datetime
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# Some cut down versions of Python may not include this module and it's not critical for us
|
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||||
|
@ -34,24 +39,25 @@ from apscheduler.triggers.interval import IntervalTrigger
|
||||||
from UniversalAnalytics import Tracker
|
from UniversalAnalytics import Tracker
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
import activity_handler
|
from plexpy import activity_handler
|
||||||
import activity_pinger
|
from plexpy import activity_pinger
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import datafactory
|
from plexpy import datafactory
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import logger
|
from plexpy import logger
|
||||||
import mobile_app
|
from plexpy import mobile_app
|
||||||
import newsletters
|
from plexpy import newsletters
|
||||||
import newsletter_handler
|
from plexpy import newsletter_handler
|
||||||
import notification_handler
|
from plexpy import notification_handler
|
||||||
import notifiers
|
from plexpy import notifiers
|
||||||
import plextv
|
from plexpy import plextv
|
||||||
import users
|
from plexpy import users
|
||||||
import versioncheck
|
from plexpy import versioncheck
|
||||||
import web_socket
|
from plexpy import web_socket
|
||||||
import webstart
|
from plexpy import webstart
|
||||||
import plexpy.config
|
from plexpy import config
|
||||||
|
|
||||||
|
|
||||||
PROG_DIR = None
|
PROG_DIR = None
|
||||||
FULL_PATH = None
|
FULL_PATH = None
|
||||||
|
@ -74,7 +80,7 @@ DOCKER = False
|
||||||
SCHED = None
|
SCHED = None
|
||||||
SCHED_LOCK = threading.Lock()
|
SCHED_LOCK = threading.Lock()
|
||||||
|
|
||||||
NOTIFY_QUEUE = Queue()
|
NOTIFY_QUEUE = queue.Queue()
|
||||||
|
|
||||||
INIT_LOCK = threading.Lock()
|
INIT_LOCK = threading.Lock()
|
||||||
_INITIALIZED = False
|
_INITIALIZED = False
|
||||||
|
@ -128,7 +134,7 @@ def initialize(config_file):
|
||||||
global UMASK
|
global UMASK
|
||||||
global _UPDATE
|
global _UPDATE
|
||||||
|
|
||||||
CONFIG = plexpy.config.Config(config_file)
|
CONFIG = config.Config(config_file)
|
||||||
CONFIG_FILE = config_file
|
CONFIG_FILE = config_file
|
||||||
|
|
||||||
assert CONFIG is not None
|
assert CONFIG is not None
|
||||||
|
@ -137,7 +143,7 @@ def initialize(config_file):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535:
|
if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535:
|
||||||
plexpy.logger.warn("HTTP_PORT out of bounds: 21 < %s < 65535", CONFIG.HTTP_PORT)
|
logger.warn("HTTP_PORT out of bounds: 21 < %s < 65535", CONFIG.HTTP_PORT)
|
||||||
CONFIG.HTTP_PORT = 8181
|
CONFIG.HTTP_PORT = 8181
|
||||||
|
|
||||||
if not CONFIG.HTTPS_CERT:
|
if not CONFIG.HTTPS_CERT:
|
||||||
|
@ -162,7 +168,7 @@ def initialize(config_file):
|
||||||
' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else ''
|
' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else ''
|
||||||
))
|
))
|
||||||
logger.info("{} (UTC{})".format(
|
logger.info("{} (UTC{})".format(
|
||||||
plexpy.SYS_TIMEZONE.zone, plexpy.SYS_UTC_OFFSET
|
SYS_TIMEZONE.zone, SYS_UTC_OFFSET
|
||||||
))
|
))
|
||||||
logger.info("Python {}".format(
|
logger.info("Python {}".format(
|
||||||
sys.version
|
sys.version
|
||||||
|
@ -379,29 +385,29 @@ def win_system_tray():
|
||||||
from systray import SysTrayIcon
|
from systray import SysTrayIcon
|
||||||
|
|
||||||
def tray_open(sysTrayIcon):
|
def tray_open(sysTrayIcon):
|
||||||
launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT, plexpy.HTTP_ROOT)
|
launch_browser(CONFIG.HTTP_HOST, HTTP_PORT, HTTP_ROOT)
|
||||||
|
|
||||||
def tray_check_update(sysTrayIcon):
|
def tray_check_update(sysTrayIcon):
|
||||||
versioncheck.check_update()
|
versioncheck.check_update()
|
||||||
|
|
||||||
def tray_update(sysTrayIcon):
|
def tray_update(sysTrayIcon):
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if UPDATE_AVAILABLE:
|
||||||
plexpy.SIGNAL = 'update'
|
SIGNAL = 'update'
|
||||||
else:
|
else:
|
||||||
hover_text = common.PRODUCT + ' - No Update Available'
|
hover_text = common.PRODUCT + ' - No Update Available'
|
||||||
plexpy.WIN_SYS_TRAY_ICON.update(hover_text=hover_text)
|
WIN_SYS_TRAY_ICON.update(hover_text=hover_text)
|
||||||
|
|
||||||
def tray_restart(sysTrayIcon):
|
def tray_restart(sysTrayIcon):
|
||||||
plexpy.SIGNAL = 'restart'
|
SIGNAL = 'restart'
|
||||||
|
|
||||||
def tray_quit(sysTrayIcon):
|
def tray_quit(sysTrayIcon):
|
||||||
plexpy.SIGNAL = 'shutdown'
|
SIGNAL = 'shutdown'
|
||||||
|
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if UPDATE_AVAILABLE:
|
||||||
icon = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images/logo_tray-update.ico')
|
icon = os.path.join(PROG_DIR, 'data/interfaces/', CONFIG.INTERFACE, 'images/logo_tray-update.ico')
|
||||||
hover_text = common.PRODUCT + ' - Update Available!'
|
hover_text = common.PRODUCT + ' - Update Available!'
|
||||||
else:
|
else:
|
||||||
icon = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images/logo_tray.ico')
|
icon = os.path.join(PROG_DIR, 'data/interfaces/', CONFIG.INTERFACE, 'images/logo_tray.ico')
|
||||||
hover_text = common.PRODUCT
|
hover_text = common.PRODUCT
|
||||||
|
|
||||||
menu_options = (('Open Tautulli', None, tray_open, 'default'),
|
menu_options = (('Open Tautulli', None, tray_open, 'default'),
|
||||||
|
@ -413,11 +419,11 @@ def win_system_tray():
|
||||||
logger.info("Launching system tray icon.")
|
logger.info("Launching system tray icon.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexpy.WIN_SYS_TRAY_ICON = SysTrayIcon(icon, hover_text, menu_options, on_quit=tray_quit)
|
WIN_SYS_TRAY_ICON = SysTrayIcon(icon, hover_text, menu_options, on_quit=tray_quit)
|
||||||
plexpy.WIN_SYS_TRAY_ICON.start()
|
WIN_SYS_TRAY_ICON.start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Unable to launch system tray icon: %s." % e)
|
logger.error("Unable to launch system tray icon: %s." % e)
|
||||||
plexpy.WIN_SYS_TRAY_ICON = None
|
WIN_SYS_TRAY_ICON = None
|
||||||
|
|
||||||
|
|
||||||
def initialize_scheduler():
|
def initialize_scheduler():
|
||||||
|
@ -2055,12 +2061,12 @@ def initialize_tracker():
|
||||||
'dataSource': 'server',
|
'dataSource': 'server',
|
||||||
'appName': common.PRODUCT,
|
'appName': common.PRODUCT,
|
||||||
'appVersion': common.RELEASE,
|
'appVersion': common.RELEASE,
|
||||||
'appId': plexpy.INSTALL_TYPE,
|
'appId': INSTALL_TYPE,
|
||||||
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,
|
'appInstallerId': CONFIG.GIT_BRANCH,
|
||||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
||||||
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
||||||
'userLanguage': plexpy.SYS_LANGUAGE,
|
'userLanguage': SYS_LANGUAGE,
|
||||||
'documentEncoding': plexpy.SYS_ENCODING,
|
'documentEncoding': SYS_ENCODING,
|
||||||
'noninteractive': True
|
'noninteractive': True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,10 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
@ -21,12 +25,12 @@ from apscheduler.triggers.date import DateTrigger
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import datafactory
|
from plexpy import datafactory
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
import notification_handler
|
from plexpy import notification_handler
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
|
|
||||||
|
|
||||||
ACTIVITY_SCHED = None
|
ACTIVITY_SCHED = None
|
||||||
|
|
|
@ -13,20 +13,23 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_handler
|
from plexpy import activity_handler
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import logger
|
from plexpy import logger
|
||||||
import notification_handler
|
from plexpy import notification_handler
|
||||||
import plextv
|
from plexpy import plextv
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import web_socket
|
from plexpy import web_socket
|
||||||
|
|
||||||
|
|
||||||
monitor_lock = threading.Lock()
|
monitor_lock = threading.Lock()
|
||||||
|
@ -223,7 +226,7 @@ def check_recently_added():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
metadata = []
|
metadata = []
|
||||||
|
|
||||||
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||||
if item['media_type'] == 'movie':
|
if item['media_type'] == 'movie':
|
||||||
metadata = pms_connect.get_metadata_details(item['rating_key'])
|
metadata = pms_connect.get_metadata_details(item['rating_key'])
|
||||||
|
@ -250,7 +253,7 @@ def check_recently_added():
|
||||||
logger.debug("Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
logger.debug("Tautulli Monitor :: Library item %s added to Plex." % str(item['rating_key']))
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
item = max(metadata, key=lambda x:x['added_at'])
|
item = max(metadata, key=lambda x:x['added_at'])
|
||||||
|
|
||||||
|
|
|
@ -13,17 +13,21 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import logger
|
from plexpy import logger
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
class ActivityProcessor(object):
|
class ActivityProcessor(object):
|
||||||
|
@ -498,7 +502,7 @@ class ActivityProcessor(object):
|
||||||
if state:
|
if state:
|
||||||
values['state'] = state
|
values['state'] = state
|
||||||
|
|
||||||
for k, v in kwargs.iteritems():
|
for k, v in kwargs.items():
|
||||||
values[k] = v
|
values[k] = v
|
||||||
|
|
||||||
keys = {'session_key': session_key}
|
keys = {'session_key': session_key}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
|
@ -17,6 +16,10 @@
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
@ -30,22 +33,22 @@ import cherrypy
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import config
|
from plexpy import config
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import logger
|
from plexpy import logger
|
||||||
import mobile_app
|
from plexpy import mobile_app
|
||||||
import notification_handler
|
from plexpy import notification_handler
|
||||||
import notifiers
|
from plexpy import notifiers
|
||||||
import newsletter_handler
|
from plexpy import newsletter_handler
|
||||||
import newsletters
|
from plexpy import newsletters
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
class API2:
|
class API2(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self._api_valid_methods = self._api_docs().keys()
|
self._api_valid_methods = list(self._api_docs().keys())
|
||||||
self._api_authenticated = False
|
self._api_authenticated = False
|
||||||
self._api_out_type = 'json' # default
|
self._api_out_type = 'json' # default
|
||||||
self._api_msg = None
|
self._api_msg = None
|
||||||
|
@ -201,7 +204,7 @@ class API2:
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# We assume this is a traceback
|
# We assume this is a traceback
|
||||||
tl = (len(templog) - 1)
|
tl = (len(templog) - 1)
|
||||||
templog[tl]['msg'] += helpers.sanitize(unicode(line.replace('\n', ''), 'utf-8'))
|
templog[tl]['msg'] += helpers.sanitize(str(line.replace('\n', ''), 'utf-8'))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
||||||
|
@ -209,7 +212,7 @@ class API2:
|
||||||
d = {
|
d = {
|
||||||
'time': temp_loglevel_and_time[0],
|
'time': temp_loglevel_and_time[0],
|
||||||
'loglevel': loglvl,
|
'loglevel': loglvl,
|
||||||
'msg': helpers.sanitize(unicode(msg.replace('\n', ''), 'utf-8')),
|
'msg': helpers.sanitize(str(msg.replace('\n', ''), 'utf-8')),
|
||||||
'thread': thread
|
'thread': thread
|
||||||
}
|
}
|
||||||
templog.append(d)
|
templog.append(d)
|
||||||
|
@ -227,7 +230,7 @@ class API2:
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
logger.api_debug("Tautulli APIv2 :: Searching log values for '%s'" % search)
|
logger.api_debug("Tautulli APIv2 :: Searching log values for '%s'" % search)
|
||||||
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
|
tt = [d for d in templog for k, v in list(d.items()) if search.lower() in v.lower()]
|
||||||
|
|
||||||
if len(tt):
|
if len(tt):
|
||||||
templog = tt
|
templog = tt
|
||||||
|
@ -235,7 +238,7 @@ class API2:
|
||||||
if regex:
|
if regex:
|
||||||
tt = []
|
tt = []
|
||||||
for l in templog:
|
for l in templog:
|
||||||
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
|
stringdict = ' '.join('{}{}'.format(k, v) for k, v in list(l.items()))
|
||||||
if reg.search(stringdict):
|
if reg.search(stringdict):
|
||||||
tt.append(l)
|
tt.append(l)
|
||||||
|
|
||||||
|
@ -271,10 +274,10 @@ class API2:
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
# Truthify the dict
|
# Truthify the dict
|
||||||
for k, v in conf.iteritems():
|
for k, v in conf.items():
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict):
|
||||||
d = {}
|
d = {}
|
||||||
for kk, vv in v.iteritems():
|
for kk, vv in v.items():
|
||||||
if vv == '0' or vv == '1':
|
if vv == '0' or vv == '1':
|
||||||
d[kk] = bool(vv)
|
d[kk] = bool(vv)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -18,12 +20,16 @@
|
||||||
#########################################
|
#########################################
|
||||||
|
|
||||||
|
|
||||||
import urllib
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
|
||||||
from common import USER_AGENT
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
|
from plexpy.common import USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
class PlexPyURLopener(urllib.FancyURLopener):
|
class PlexPyURLopener(urllib.request.FancyURLopener):
|
||||||
version = USER_AGENT
|
version = USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +50,7 @@ class AuthURLOpener(PlexPyURLopener):
|
||||||
self.numTries = 0
|
self.numTries = 0
|
||||||
|
|
||||||
# call the base class
|
# call the base class
|
||||||
urllib.FancyURLopener.__init__(self)
|
urllib.request.FancyURLopener.__init__(self)
|
||||||
|
|
||||||
def prompt_user_passwd(self, host, realm):
|
def prompt_user_passwd(self, host, realm):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -13,17 +15,21 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import distro
|
||||||
import platform
|
import platform
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import version
|
from plexpy import version
|
||||||
|
|
||||||
|
|
||||||
# Identify Our Application
|
# Identify Our Application
|
||||||
PRODUCT = 'Tautulli'
|
PRODUCT = 'Tautulli'
|
||||||
PLATFORM = platform.system()
|
PLATFORM = platform.system()
|
||||||
PLATFORM_RELEASE = platform.release()
|
PLATFORM_RELEASE = platform.release()
|
||||||
PLATFORM_VERSION = platform.version()
|
PLATFORM_VERSION = platform.version()
|
||||||
PLATFORM_LINUX_DISTRO = ' '.join(x for x in platform.linux_distribution() if x)
|
PLATFORM_LINUX_DISTRO = ' '.join(x for x in distro.linux_distribution() if x)
|
||||||
PLATFORM_DEVICE_NAME = platform.node()
|
PLATFORM_DEVICE_NAME = platform.node()
|
||||||
BRANCH = version.PLEXPY_BRANCH
|
BRANCH = version.PLEXPY_BRANCH
|
||||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||||
|
@ -98,7 +104,7 @@ PLATFORM_NAMES = {
|
||||||
'xbmc': 'xbmc',
|
'xbmc': 'xbmc',
|
||||||
'xbox': 'xbox'
|
'xbox': 'xbox'
|
||||||
}
|
}
|
||||||
PLATFORM_NAMES = OrderedDict(sorted(PLATFORM_NAMES.items(), key=lambda k: k[0], reverse=True))
|
PLATFORM_NAMES = OrderedDict(sorted(list(PLATFORM_NAMES.items()), key=lambda k: k[0], reverse=True))
|
||||||
|
|
||||||
MEDIA_FLAGS_AUDIO = {
|
MEDIA_FLAGS_AUDIO = {
|
||||||
'ac.?3': 'dolby_digital',
|
'ac.?3': 'dolby_digital',
|
||||||
|
@ -147,7 +153,7 @@ VIDEO_QUALITY_PROFILES = {
|
||||||
96: '0.096 Mbps',
|
96: '0.096 Mbps',
|
||||||
64: '0.064 Mbps'
|
64: '0.064 Mbps'
|
||||||
}
|
}
|
||||||
VIDEO_QUALITY_PROFILES = OrderedDict(sorted(VIDEO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True))
|
VIDEO_QUALITY_PROFILES = OrderedDict(sorted(list(VIDEO_QUALITY_PROFILES.items()), key=lambda k: k[0], reverse=True))
|
||||||
|
|
||||||
AUDIO_QUALITY_PROFILES = {
|
AUDIO_QUALITY_PROFILES = {
|
||||||
512: '512 kbps',
|
512: '512 kbps',
|
||||||
|
@ -157,7 +163,7 @@ AUDIO_QUALITY_PROFILES = {
|
||||||
128: '128 kbps',
|
128: '128 kbps',
|
||||||
96: '96 kbps'
|
96: '96 kbps'
|
||||||
}
|
}
|
||||||
AUDIO_QUALITY_PROFILES = OrderedDict(sorted(AUDIO_QUALITY_PROFILES.items(), key=lambda k: k[0], reverse=True))
|
AUDIO_QUALITY_PROFILES = OrderedDict(sorted(list(AUDIO_QUALITY_PROFILES.items()), key=lambda k: k[0], reverse=True))
|
||||||
|
|
||||||
HW_DECODERS = [
|
HW_DECODERS = [
|
||||||
'dxva2',
|
'dxva2',
|
||||||
|
|
110
plexpy/config.py
110
plexpy/config.py
|
@ -13,6 +13,10 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from past.builtins import basestring
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -22,7 +26,7 @@ import time
|
||||||
from configobj import ConfigObj
|
from configobj import ConfigObj
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
def bool_int(value):
|
def bool_int(value):
|
||||||
|
@ -49,7 +53,7 @@ _CONFIG_DEFINITIONS = {
|
||||||
'PMS_IS_REMOTE': (int, 'PMS', 0),
|
'PMS_IS_REMOTE': (int, 'PMS', 0),
|
||||||
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
|
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
|
||||||
'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000),
|
'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000),
|
||||||
'PMS_NAME': (unicode, 'PMS', ''),
|
'PMS_NAME': (str, 'PMS', ''),
|
||||||
'PMS_PORT': (int, 'PMS', 32400),
|
'PMS_PORT': (int, 'PMS', 32400),
|
||||||
'PMS_TOKEN': (str, 'PMS', ''),
|
'PMS_TOKEN': (str, 'PMS', ''),
|
||||||
'PMS_SSL': (int, 'PMS', 0),
|
'PMS_SSL': (int, 'PMS', 0),
|
||||||
|
@ -345,35 +349,35 @@ _CONFIG_DEFINITIONS = {
|
||||||
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
||||||
'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2),
|
'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2),
|
||||||
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
|
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
|
||||||
'NOTIFY_ON_START_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_START_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_START_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) started playing {title}.'),
|
'NOTIFY_ON_START_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) started playing {title}.'),
|
||||||
'NOTIFY_ON_STOP_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_STOP_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_STOP_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has stopped {title}.'),
|
'NOTIFY_ON_STOP_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has stopped {title}.'),
|
||||||
'NOTIFY_ON_PAUSE_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_PAUSE_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_PAUSE_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has paused {title}.'),
|
'NOTIFY_ON_PAUSE_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has paused {title}.'),
|
||||||
'NOTIFY_ON_RESUME_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_RESUME_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_RESUME_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has resumed {title}.'),
|
'NOTIFY_ON_RESUME_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has resumed {title}.'),
|
||||||
'NOTIFY_ON_BUFFER_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_BUFFER_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_BUFFER_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) is buffering {title}.'),
|
'NOTIFY_ON_BUFFER_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) is buffering {title}.'),
|
||||||
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_WATCHED_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has watched {title}.'),
|
'NOTIFY_ON_WATCHED_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has watched {title}.'),
|
||||||
'NOTIFY_ON_CREATED_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_CREATED_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_CREATED_BODY_TEXT': (unicode, 'Monitoring', '{title} was recently added to Plex.'),
|
'NOTIFY_ON_CREATED_BODY_TEXT': (str, 'Monitoring', '{title} was recently added to Plex.'),
|
||||||
'NOTIFY_ON_EXTDOWN_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_EXTDOWN_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_EXTDOWN_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is down.'),
|
'NOTIFY_ON_EXTDOWN_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server remote access is down.'),
|
||||||
'NOTIFY_ON_INTDOWN_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_INTDOWN_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_INTDOWN_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is down.'),
|
'NOTIFY_ON_INTDOWN_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server is down.'),
|
||||||
'NOTIFY_ON_EXTUP_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_EXTUP_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_EXTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is back up.'),
|
'NOTIFY_ON_EXTUP_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server remote access is back up.'),
|
||||||
'NOTIFY_ON_INTUP_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_INTUP_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_INTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is back up.'),
|
'NOTIFY_ON_INTUP_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server is back up.'),
|
||||||
'NOTIFY_ON_PMSUPDATE_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_PMSUPDATE_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_PMSUPDATE_BODY_TEXT': (unicode, 'Monitoring', 'An update is available for the Plex Media Server (version {update_version}).'),
|
'NOTIFY_ON_PMSUPDATE_BODY_TEXT': (str, 'Monitoring', 'An update is available for the Plex Media Server (version {update_version}).'),
|
||||||
'NOTIFY_ON_CONCURRENT_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_CONCURRENT_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_CONCURRENT_BODY_TEXT': (unicode, 'Monitoring', '{user} has {user_streams} concurrent streams.'),
|
'NOTIFY_ON_CONCURRENT_BODY_TEXT': (str, 'Monitoring', '{user} has {user_streams} concurrent streams.'),
|
||||||
'NOTIFY_ON_NEWDEVICE_SUBJECT_TEXT': (unicode, 'Monitoring', 'Tautulli ({server_name})'),
|
'NOTIFY_ON_NEWDEVICE_SUBJECT_TEXT': (str, 'Monitoring', 'Tautulli ({server_name})'),
|
||||||
'NOTIFY_ON_NEWDEVICE_BODY_TEXT': (unicode, 'Monitoring', '{user} is streaming from a new device: {player}.'),
|
'NOTIFY_ON_NEWDEVICE_BODY_TEXT': (str, 'Monitoring', '{user} is streaming from a new device: {player}.'),
|
||||||
'NOTIFY_SCRIPTS_ARGS_TEXT': (unicode, 'Monitoring', ''),
|
'NOTIFY_SCRIPTS_ARGS_TEXT': (str, 'Monitoring', ''),
|
||||||
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/Tautulli'),
|
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/Tautulli'),
|
||||||
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
||||||
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
|
||||||
|
@ -512,7 +516,7 @@ _CONFIG_DEFINITIONS = {
|
||||||
'SLACK_ON_CONCURRENT': (int, 'Slack', 0),
|
'SLACK_ON_CONCURRENT': (int, 'Slack', 0),
|
||||||
'SLACK_ON_NEWDEVICE': (int, 'Slack', 0),
|
'SLACK_ON_NEWDEVICE': (int, 'Slack', 0),
|
||||||
'SCRIPTS_ENABLED': (int, 'Scripts', 0),
|
'SCRIPTS_ENABLED': (int, 'Scripts', 0),
|
||||||
'SCRIPTS_FOLDER': (unicode, 'Scripts', ''),
|
'SCRIPTS_FOLDER': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_TIMEOUT': (int, 'Scripts', 30),
|
'SCRIPTS_TIMEOUT': (int, 'Scripts', 30),
|
||||||
'SCRIPTS_ON_PLAY': (int, 'Scripts', 0),
|
'SCRIPTS_ON_PLAY': (int, 'Scripts', 0),
|
||||||
'SCRIPTS_ON_STOP': (int, 'Scripts', 0),
|
'SCRIPTS_ON_STOP': (int, 'Scripts', 0),
|
||||||
|
@ -528,20 +532,20 @@ _CONFIG_DEFINITIONS = {
|
||||||
'SCRIPTS_ON_PMSUPDATE': (int, 'Scripts', 0),
|
'SCRIPTS_ON_PMSUPDATE': (int, 'Scripts', 0),
|
||||||
'SCRIPTS_ON_CONCURRENT': (int, 'Scripts', 0),
|
'SCRIPTS_ON_CONCURRENT': (int, 'Scripts', 0),
|
||||||
'SCRIPTS_ON_NEWDEVICE': (int, 'Scripts', 0),
|
'SCRIPTS_ON_NEWDEVICE': (int, 'Scripts', 0),
|
||||||
'SCRIPTS_ON_PLAY_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_PLAY_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_STOP_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_STOP_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_PAUSE_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_PAUSE_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_RESUME_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_RESUME_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_BUFFER_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_BUFFER_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_WATCHED_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_WATCHED_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_CREATED_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_CREATED_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_EXTDOWN_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_EXTDOWN_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_EXTUP_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_EXTUP_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_INTDOWN_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_INTDOWN_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_INTUP_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_INTUP_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_PMSUPDATE_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_PMSUPDATE_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_CONCURRENT_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_CONCURRENT_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SCRIPTS_ON_NEWDEVICE_SCRIPT': (unicode, 'Scripts', ''),
|
'SCRIPTS_ON_NEWDEVICE_SCRIPT': (str, 'Scripts', ''),
|
||||||
'SYNCHRONOUS_MODE': (str, 'Advanced', 'NORMAL'),
|
'SYNCHRONOUS_MODE': (str, 'Advanced', 'NORMAL'),
|
||||||
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
||||||
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
||||||
|
@ -680,7 +684,7 @@ class Config(object):
|
||||||
""" Initialize the config with values from a file """
|
""" Initialize the config with values from a file """
|
||||||
self._config_file = config_file
|
self._config_file = config_file
|
||||||
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
||||||
for key in _CONFIG_DEFINITIONS.keys():
|
for key in list(_CONFIG_DEFINITIONS.keys()):
|
||||||
self.check_setting(key)
|
self.check_setting(key)
|
||||||
self._upgrade()
|
self._upgrade()
|
||||||
self._blacklist()
|
self._blacklist()
|
||||||
|
@ -689,8 +693,8 @@ class Config(object):
|
||||||
""" Add tokens and passwords to blacklisted words in logger """
|
""" Add tokens and passwords to blacklisted words in logger """
|
||||||
blacklist = set()
|
blacklist = set()
|
||||||
|
|
||||||
for key, subkeys in self._config.iteritems():
|
for key, subkeys in self._config.items():
|
||||||
for subkey, value in subkeys.iteritems():
|
for subkey, value in subkeys.items():
|
||||||
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
||||||
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
|
subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
|
||||||
blacklist.add(value.strip())
|
blacklist.add(value.strip())
|
||||||
|
@ -733,14 +737,14 @@ class Config(object):
|
||||||
|
|
||||||
# first copy over everything from the old config, even if it is not
|
# first copy over everything from the old config, even if it is not
|
||||||
# correctly defined to keep from losing data
|
# correctly defined to keep from losing data
|
||||||
for key, subkeys in self._config.items():
|
for key, subkeys in list(self._config.items()):
|
||||||
if key not in new_config:
|
if key not in new_config:
|
||||||
new_config[key] = {}
|
new_config[key] = {}
|
||||||
for subkey, value in subkeys.items():
|
for subkey, value in list(subkeys.items()):
|
||||||
new_config[key][subkey] = value
|
new_config[key][subkey] = value
|
||||||
|
|
||||||
# next make sure that everything we expect to have defined is so
|
# next make sure that everything we expect to have defined is so
|
||||||
for key in _CONFIG_DEFINITIONS.keys():
|
for key in list(_CONFIG_DEFINITIONS.keys()):
|
||||||
key, definition_type, section, ini_key, default = self._define(key)
|
key, definition_type, section, ini_key, default = self._define(key)
|
||||||
self.check_setting(key)
|
self.check_setting(key)
|
||||||
if section not in new_config:
|
if section not in new_config:
|
||||||
|
@ -784,7 +788,7 @@ class Config(object):
|
||||||
"""
|
"""
|
||||||
Given a big bunch of key value pairs, apply them to the ini.
|
Given a big bunch of key value pairs, apply them to the ini.
|
||||||
"""
|
"""
|
||||||
for name, value in kwargs.items():
|
for name, value in list(kwargs.items()):
|
||||||
key, definition_type, section, ini_key, default = self._define(name)
|
key, definition_type, section, ini_key, default = self._define(name)
|
||||||
self._config[section][ini_key] = definition_type(value)
|
self._config[section][ini_key] = definition_type(value)
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,9 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
@ -21,7 +24,8 @@ import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
FILENAME = "tautulli.db"
|
FILENAME = "tautulli.db"
|
||||||
db_lock = threading.Lock()
|
db_lock = threading.Lock()
|
||||||
|
@ -198,21 +202,21 @@ class MonitorDatabase(object):
|
||||||
trans_type = 'update'
|
trans_type = 'update'
|
||||||
changes_before = self.connection.total_changes
|
changes_before = self.connection.total_changes
|
||||||
|
|
||||||
gen_params = lambda my_dict: [x + " = ?" for x in my_dict.keys()]
|
gen_params = lambda my_dict: [x + " = ?" for x in list(my_dict.keys())]
|
||||||
|
|
||||||
update_query = "UPDATE " + table_name + " SET " + ", ".join(gen_params(value_dict)) + \
|
update_query = "UPDATE " + table_name + " SET " + ", ".join(gen_params(value_dict)) + \
|
||||||
" WHERE " + " AND ".join(gen_params(key_dict))
|
" WHERE " + " AND ".join(gen_params(key_dict))
|
||||||
|
|
||||||
self.action(update_query, value_dict.values() + key_dict.values())
|
self.action(update_query, list(value_dict.values()) + list(key_dict.values()))
|
||||||
|
|
||||||
if self.connection.total_changes == changes_before:
|
if self.connection.total_changes == changes_before:
|
||||||
trans_type = 'insert'
|
trans_type = 'insert'
|
||||||
insert_query = (
|
insert_query = (
|
||||||
"INSERT INTO " + table_name + " (" + ", ".join(value_dict.keys() + key_dict.keys()) + ")" +
|
"INSERT INTO " + table_name + " (" + ", ".join(list(value_dict.keys()) + list(key_dict.keys())) + ")" +
|
||||||
" VALUES (" + ", ".join(["?"] * len(value_dict.keys() + key_dict.keys())) + ")"
|
" VALUES (" + ", ".join(["?"] * len(list(value_dict.keys()) + list(key_dict.keys()))) + ")"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.action(insert_query, value_dict.values() + key_dict.values())
|
self.action(insert_query, list(value_dict.values()) + list(key_dict.values()))
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
logger.info("Tautulli Database :: Queries failed: %s and %s", update_query, insert_query)
|
logger.info("Tautulli Database :: Queries failed: %s and %s", update_query, insert_query)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,17 +15,24 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
from past.utils import old_div
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import datatables
|
from plexpy import datatables
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import session
|
from plexpy import session
|
||||||
|
|
||||||
|
|
||||||
class DataFactory(object):
|
class DataFactory(object):
|
||||||
|
@ -208,7 +217,7 @@ class DataFactory(object):
|
||||||
|
|
||||||
if item['percent_complete'] >= watched_percent[item['media_type']]:
|
if item['percent_complete'] >= watched_percent[item['media_type']]:
|
||||||
watched_status = 1
|
watched_status = 1
|
||||||
elif item['percent_complete'] >= watched_percent[item['media_type']]/2:
|
elif item['percent_complete'] >= old_div(watched_percent[item['media_type']],2):
|
||||||
watched_status = 0.5
|
watched_status = 0.5
|
||||||
else:
|
else:
|
||||||
watched_status = 0
|
watched_status = 0
|
||||||
|
@ -655,7 +664,7 @@ class DataFactory(object):
|
||||||
for item in result:
|
for item in result:
|
||||||
# Rename Mystery platform names
|
# Rename Mystery platform names
|
||||||
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
||||||
platform_name = next((v for k, v in common.PLATFORM_NAMES.iteritems() if k in platform.lower()), 'default')
|
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default')
|
||||||
|
|
||||||
row = {'total_plays': item['total_plays'],
|
row = {'total_plays': item['total_plays'],
|
||||||
'total_duration': item['total_duration'],
|
'total_duration': item['total_duration'],
|
||||||
|
@ -750,7 +759,7 @@ class DataFactory(object):
|
||||||
for item in result:
|
for item in result:
|
||||||
times.append({'time': str(item['started']) + 'B', 'count': 1})
|
times.append({'time': str(item['started']) + 'B', 'count': 1})
|
||||||
times.append({'time': str(item['stopped']) + 'A', 'count': -1})
|
times.append({'time': str(item['stopped']) + 'A', 'count': -1})
|
||||||
times = sorted(times, key=lambda k: k['time'])
|
times = sorted(times, key=lambda k: k['time'])
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
last_count = 0
|
last_count = 0
|
||||||
|
@ -985,7 +994,7 @@ class DataFactory(object):
|
||||||
'pre_tautulli': pre_tautulli
|
'pre_tautulli': pre_tautulli
|
||||||
}
|
}
|
||||||
|
|
||||||
stream_output = {k: v or '' for k, v in stream_output.iteritems()}
|
stream_output = {k: v or '' for k, v in stream_output.items()}
|
||||||
return stream_output
|
return stream_output
|
||||||
|
|
||||||
def get_metadata_details(self, rating_key):
|
def get_metadata_details(self, rating_key):
|
||||||
|
@ -1079,7 +1088,7 @@ class DataFactory(object):
|
||||||
metadata_list.append(metadata)
|
metadata_list.append(metadata)
|
||||||
|
|
||||||
filtered_metadata_list = session.filter_session_info(metadata_list, filter_key='section_id')
|
filtered_metadata_list = session.filter_session_info(metadata_list, filter_key='section_id')
|
||||||
|
|
||||||
if filtered_metadata_list:
|
if filtered_metadata_list:
|
||||||
return filtered_metadata_list[0]
|
return filtered_metadata_list[0]
|
||||||
else:
|
else:
|
||||||
|
@ -1093,7 +1102,7 @@ class DataFactory(object):
|
||||||
where = 'WHERE ' + ' AND '.join([w[0] + ' = "' + w[1] + '"' for w in custom_where])
|
where = 'WHERE ' + ' AND '.join([w[0] + ' = "' + w[1] + '"' for w in custom_where])
|
||||||
else:
|
else:
|
||||||
where = ''
|
where = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = 'SELECT SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - ' \
|
query = 'SELECT SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - ' \
|
||||||
'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS total_duration ' \
|
'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS total_duration ' \
|
||||||
|
@ -1518,7 +1527,7 @@ class DataFactory(object):
|
||||||
# function to map rating keys pairs
|
# function to map rating keys pairs
|
||||||
def get_pairs(old, new):
|
def get_pairs(old, new):
|
||||||
pairs = {}
|
pairs = {}
|
||||||
for k, v in old.iteritems():
|
for k, v in old.items():
|
||||||
if k in new:
|
if k in new:
|
||||||
pairs.update({v['rating_key']: new[k]['rating_key']})
|
pairs.update({v['rating_key']: new[k]['rating_key']})
|
||||||
if 'children' in old[k]:
|
if 'children' in old[k]:
|
||||||
|
@ -1533,27 +1542,27 @@ class DataFactory(object):
|
||||||
|
|
||||||
if mapping:
|
if mapping:
|
||||||
logger.info("Tautulli DataFactory :: Updating metadata in the database.")
|
logger.info("Tautulli DataFactory :: Updating metadata in the database.")
|
||||||
for old_key, new_key in mapping.iteritems():
|
for old_key, new_key in mapping.items():
|
||||||
metadata = pms_connect.get_metadata_details(new_key)
|
metadata = pms_connect.get_metadata_details(new_key)
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
|
if metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
|
||||||
# check grandparent_rating_key (2 tables)
|
# check grandparent_rating_key (2 tables)
|
||||||
monitor_db.action('UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
monitor_db.action('UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
||||||
[new_key, old_key])
|
[new_key, old_key])
|
||||||
monitor_db.action('UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
monitor_db.action('UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?',
|
||||||
[new_key, old_key])
|
[new_key, old_key])
|
||||||
elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album':
|
elif metadata['media_type'] == 'season' or metadata['media_type'] == 'album':
|
||||||
# check parent_rating_key (2 tables)
|
# check parent_rating_key (2 tables)
|
||||||
monitor_db.action('UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
monitor_db.action('UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
||||||
[new_key, old_key])
|
[new_key, old_key])
|
||||||
monitor_db.action('UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
monitor_db.action('UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?',
|
||||||
[new_key, old_key])
|
[new_key, old_key])
|
||||||
else:
|
else:
|
||||||
# check rating_key (2 tables)
|
# check rating_key (2 tables)
|
||||||
monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?',
|
monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?',
|
||||||
[new_key, old_key])
|
[new_key, old_key])
|
||||||
monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?',
|
monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?',
|
||||||
[new_key, old_key])
|
[new_key, old_key])
|
||||||
|
|
||||||
# update session_history_metadata table
|
# update session_history_metadata table
|
||||||
|
@ -1601,7 +1610,7 @@ class DataFactory(object):
|
||||||
metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'],
|
metadata['media_index'], metadata['parent_media_index'], metadata['section_id'], metadata['thumb'],
|
||||||
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
|
metadata['parent_thumb'], metadata['grandparent_thumb'], metadata['art'], metadata['media_type'],
|
||||||
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
metadata['year'], metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
||||||
metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['tagline'],
|
metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['tagline'],
|
||||||
metadata['rating'], metadata['duration'], metadata['guid'], directors, writers, actors, genres,
|
metadata['rating'], metadata['duration'], metadata['guid'], directors, writers, actors, genres,
|
||||||
metadata['studio'], labels,
|
metadata['studio'], labels,
|
||||||
old_rating_key]
|
old_rating_key]
|
||||||
|
|
|
@ -13,11 +13,14 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
class DataTables(object):
|
class DataTables(object):
|
||||||
|
@ -90,7 +93,7 @@ class DataTables(object):
|
||||||
filtered = self.ssp_db.select(query, args=args)
|
filtered = self.ssp_db.select(query, args=args)
|
||||||
|
|
||||||
# Remove NULL rows
|
# Remove NULL rows
|
||||||
filtered = [row for row in filtered if not all(v is None for v in row.values())]
|
filtered = [row for row in filtered if not all(v is None for v in list(row.values()))]
|
||||||
|
|
||||||
# Build grand totals
|
# Build grand totals
|
||||||
totalcount = self.ssp_db.select('SELECT COUNT(id) as total_count from %s' % table_name)[0]['total_count']
|
totalcount = self.ssp_db.select('SELECT COUNT(id) as total_count from %s' % table_name)[0]['total_count']
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,13 +15,18 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
from builtins import range
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import logger
|
from plexpy import logger
|
||||||
import session
|
from plexpy import session
|
||||||
|
|
||||||
|
|
||||||
class Graphs(object):
|
class Graphs(object):
|
||||||
|
@ -32,7 +39,7 @@ class Graphs(object):
|
||||||
|
|
||||||
if not time_range.isdigit():
|
if not time_range.isdigit():
|
||||||
time_range = '30'
|
time_range = '30'
|
||||||
|
|
||||||
user_cond = ''
|
user_cond = ''
|
||||||
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
|
||||||
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
|
||||||
|
@ -44,7 +51,7 @@ class Graphs(object):
|
||||||
|
|
||||||
group_by = 'reference_id' if grouping else 'id'
|
group_by = 'reference_id' if grouping else 'id'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if y_axis == 'plays':
|
if y_axis == 'plays':
|
||||||
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
|
query = 'SELECT date(started, "unixepoch", "localtime") AS date_played, ' \
|
||||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,6 +15,16 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from past.builtins import cmp
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import zip
|
||||||
|
from builtins import str
|
||||||
|
from past.builtins import basestring
|
||||||
|
from past.utils import old_div
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import cloudinary
|
import cloudinary
|
||||||
from cloudinary.api import delete_resources_by_tag
|
from cloudinary.api import delete_resources_by_tag
|
||||||
|
@ -20,12 +32,15 @@ from cloudinary.uploader import upload
|
||||||
from cloudinary.utils import cloudinary_url
|
from cloudinary.utils import cloudinary_url
|
||||||
import datetime
|
import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import geoip2.database, geoip2.errors
|
import geoip2.database
|
||||||
|
import geoip2.errors
|
||||||
import gzip
|
import gzip
|
||||||
import hashlib
|
import hashlib
|
||||||
import imghdr
|
import imghdr
|
||||||
from itertools import izip_longest
|
from itertools import zip_longest
|
||||||
import ipwhois, ipwhois.exceptions, ipwhois.utils
|
import ipwhois
|
||||||
|
import ipwhois.exceptions
|
||||||
|
import ipwhois.utils
|
||||||
from IPy import IP
|
from IPy import IP
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
@ -38,13 +53,18 @@ import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib, urllib2
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import logger
|
from plexpy import logger
|
||||||
import request
|
from plexpy import request
|
||||||
from plexpy.api2 import API2
|
from plexpy.api2 import API2
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,7 +178,7 @@ def latinToAscii(unicrap):
|
||||||
|
|
||||||
def convert_milliseconds(ms):
|
def convert_milliseconds(ms):
|
||||||
|
|
||||||
seconds = ms / 1000
|
seconds = old_div(ms, 1000)
|
||||||
gmtime = time.gmtime(seconds)
|
gmtime = time.gmtime(seconds)
|
||||||
if seconds > 3600:
|
if seconds > 3600:
|
||||||
minutes = time.strftime("%H:%M:%S", gmtime)
|
minutes = time.strftime("%H:%M:%S", gmtime)
|
||||||
|
@ -172,7 +192,7 @@ def convert_milliseconds_to_minutes(ms):
|
||||||
|
|
||||||
if str(ms).isdigit():
|
if str(ms).isdigit():
|
||||||
seconds = float(ms) / 1000
|
seconds = float(ms) / 1000
|
||||||
minutes = round(seconds / 60, 0)
|
minutes = round(old_div(seconds, 60), 0)
|
||||||
|
|
||||||
return math.trunc(minutes)
|
return math.trunc(minutes)
|
||||||
|
|
||||||
|
@ -224,9 +244,9 @@ def human_duration(s, sig='dhms'):
|
||||||
hd = ''
|
hd = ''
|
||||||
|
|
||||||
if str(s).isdigit() and s > 0:
|
if str(s).isdigit() and s > 0:
|
||||||
d = int(s / 86400)
|
d = int(old_div(s, 86400))
|
||||||
h = int((s % 86400) / 3600)
|
h = int(old_div((s % 86400), 3600))
|
||||||
m = int(((s % 86400) % 3600) / 60)
|
m = int(old_div(((s % 86400) % 3600), 60))
|
||||||
s = int(((s % 86400) % 3600) % 60)
|
s = int(((s % 86400) % 3600) % 60)
|
||||||
|
|
||||||
hd_list = []
|
hd_list = []
|
||||||
|
@ -277,7 +297,7 @@ def get_age(date):
|
||||||
|
|
||||||
def bytes_to_mb(bytes):
|
def bytes_to_mb(bytes):
|
||||||
|
|
||||||
mb = int(bytes) / 1048576
|
mb = old_div(int(bytes), 1048576)
|
||||||
size = '%.1f MB' % mb
|
size = '%.1f MB' % mb
|
||||||
return size
|
return size
|
||||||
|
|
||||||
|
@ -318,7 +338,7 @@ def replace_all(text, dic, normalize=False):
|
||||||
if not text:
|
if not text:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
for i, j in dic.iteritems():
|
for i, j in dic.items():
|
||||||
if normalize:
|
if normalize:
|
||||||
try:
|
try:
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
|
@ -476,7 +496,7 @@ def get_percent(value1, value2):
|
||||||
value2 = cast_to_float(value2)
|
value2 = cast_to_float(value2)
|
||||||
|
|
||||||
if value1 != 0 and value2 != 0:
|
if value1 != 0 and value2 != 0:
|
||||||
percent = (value1 / value2) * 100
|
percent = (old_div(value1, value2)) * 100
|
||||||
else:
|
else:
|
||||||
percent = 0
|
percent = 0
|
||||||
|
|
||||||
|
@ -543,11 +563,11 @@ def sanitize_out(*dargs, **dkwargs):
|
||||||
|
|
||||||
def sanitize(obj):
|
def sanitize(obj):
|
||||||
if isinstance(obj, basestring):
|
if isinstance(obj, basestring):
|
||||||
return unicode(obj).replace('<', '<').replace('>', '>')
|
return str(obj).replace('<', '<').replace('>', '>')
|
||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
return [sanitize(o) for o in obj]
|
return [sanitize(o) for o in obj]
|
||||||
elif isinstance(obj, dict):
|
elif isinstance(obj, dict):
|
||||||
return {k: sanitize(v) for k, v in obj.iteritems()}
|
return {k: sanitize(v) for k, v in obj.items()}
|
||||||
elif isinstance(obj, tuple):
|
elif isinstance(obj, tuple):
|
||||||
return tuple(sanitize(list(obj)))
|
return tuple(sanitize(list(obj)))
|
||||||
else:
|
else:
|
||||||
|
@ -596,9 +616,9 @@ def install_geoip_db():
|
||||||
# Retrieve the GeoLite2 gzip file
|
# Retrieve the GeoLite2 gzip file
|
||||||
logger.debug("Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
|
logger.debug("Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
|
||||||
try:
|
try:
|
||||||
maxmind = urllib.URLopener()
|
maxmind = urllib.request.URLopener()
|
||||||
maxmind.retrieve(maxmind_url + geolite2_gz, temp_gz)
|
maxmind.retrieve(maxmind_url + geolite2_gz, temp_gz)
|
||||||
md5_checksum = urllib2.urlopen(maxmind_url + geolite2_md5).read()
|
md5_checksum = urllib.request.urlopen(maxmind_url + geolite2_md5).read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
|
logger.error("Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
@ -1151,7 +1171,7 @@ def grouper(iterable, n, fillvalue=None):
|
||||||
"Collect data into fixed-length chunks or blocks"
|
"Collect data into fixed-length chunks or blocks"
|
||||||
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
|
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
|
||||||
args = [iter(iterable)] * n
|
args = [iter(iterable)] * n
|
||||||
return izip_longest(fillvalue=fillvalue, *args)
|
return zip_longest(fillvalue=fillvalue, *args)
|
||||||
|
|
||||||
|
|
||||||
def traverse_map(obj, func):
|
def traverse_map(obj, func):
|
||||||
|
@ -1162,7 +1182,7 @@ def traverse_map(obj, func):
|
||||||
|
|
||||||
elif isinstance(obj, dict):
|
elif isinstance(obj, dict):
|
||||||
new_obj = {}
|
new_obj = {}
|
||||||
for k, v in obj.iteritems():
|
for k, v in obj.items():
|
||||||
new_obj[traverse_map(k, func)] = traverse_map(v, func)
|
new_obj[traverse_map(k, func)] = traverse_map(v, func)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -1187,7 +1207,7 @@ def mask_config_passwords(config):
|
||||||
cfg['value'] = ' '
|
cfg['value'] = ' '
|
||||||
|
|
||||||
elif isinstance(config, dict):
|
elif isinstance(config, dict):
|
||||||
for cfg, val in config.iteritems():
|
for cfg, val in config.items():
|
||||||
# Check for a password config keys and if the password is not blank
|
# Check for a password config keys and if the password is not blank
|
||||||
if 'password' in cfg and val != '':
|
if 'password' in cfg and val != '':
|
||||||
# Set the password to blank so it is not exposed in the HTML form
|
# Set the password to blank so it is not exposed in the HTML form
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
|
@ -16,16 +15,22 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from past.builtins import basestring
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from multiprocessing.dummy import Pool as ThreadPool
|
from multiprocessing.dummy import Pool as ThreadPool
|
||||||
from urlparse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import certifi
|
import certifi
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
class HTTPHandler(object):
|
class HTTPHandler(object):
|
||||||
|
@ -78,7 +83,7 @@ class HTTPHandler(object):
|
||||||
Output: list
|
Output: list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.uri = uri.encode('utf-8')
|
self.uri = uri
|
||||||
self.request_type = request_type.upper()
|
self.request_type = request_type.upper()
|
||||||
self.output_format = output_format.lower()
|
self.output_format = output_format.lower()
|
||||||
self.return_type = return_type
|
self.return_type = return_type
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,18 +15,23 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
from builtins import next
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import datatables
|
from plexpy import datatables
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
import plextv
|
from plexpy import plextv
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import session
|
from plexpy import session
|
||||||
|
|
||||||
|
|
||||||
def refresh_libraries():
|
def refresh_libraries():
|
||||||
|
@ -121,7 +128,7 @@ def update_section_ids():
|
||||||
for library in library_results:
|
for library in library_results:
|
||||||
section_id = library['section_id']
|
section_id = library['section_id']
|
||||||
section_type = library['section_type']
|
section_type = library['section_type']
|
||||||
|
|
||||||
if section_type != 'photo':
|
if section_type != 'photo':
|
||||||
library_children = pms_connect.get_library_children_details(section_id=section_id,
|
library_children = pms_connect.get_library_children_details(section_id=section_id,
|
||||||
section_type=section_type)
|
section_type=section_type)
|
||||||
|
@ -135,7 +142,7 @@ def update_section_ids():
|
||||||
for item in history_results:
|
for item in history_results:
|
||||||
rating_key = item['grandparent_rating_key'] if item['media_type'] != 'movie' else item['rating_key']
|
rating_key = item['grandparent_rating_key'] if item['media_type'] != 'movie' else item['rating_key']
|
||||||
section_id = key_mappings.get(str(rating_key), None)
|
section_id = key_mappings.get(str(rating_key), None)
|
||||||
|
|
||||||
if section_id:
|
if section_id:
|
||||||
try:
|
try:
|
||||||
section_keys = {'id': item['id']}
|
section_keys = {'id': item['id']}
|
||||||
|
@ -187,7 +194,7 @@ def update_labels():
|
||||||
for library in library_results:
|
for library in library_results:
|
||||||
section_id = library['section_id']
|
section_id = library['section_id']
|
||||||
section_type = library['section_type']
|
section_type = library['section_type']
|
||||||
|
|
||||||
if section_type != 'photo':
|
if section_type != 'photo':
|
||||||
library_children = []
|
library_children = []
|
||||||
library_labels = pms_connect.get_library_label_details(section_id=section_id)
|
library_labels = pms_connect.get_library_label_details(section_id=section_id)
|
||||||
|
@ -213,7 +220,7 @@ def update_labels():
|
||||||
% section_id)
|
% section_id)
|
||||||
|
|
||||||
error_keys = set()
|
error_keys = set()
|
||||||
for rating_key, labels in key_mappings.iteritems():
|
for rating_key, labels in key_mappings.items():
|
||||||
try:
|
try:
|
||||||
labels = ';'.join(labels)
|
labels = ';'.join(labels)
|
||||||
monitor_db.action('UPDATE session_history_metadata SET labels = ? '
|
monitor_db.action('UPDATE session_history_metadata SET labels = ? '
|
||||||
|
@ -309,7 +316,7 @@ class Libraries(object):
|
||||||
return default_return
|
return default_return
|
||||||
|
|
||||||
result = query['result']
|
result = query['result']
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for item in result:
|
for item in result:
|
||||||
if item['media_type'] == 'episode' and item['parent_thumb']:
|
if item['media_type'] == 'episode' and item['parent_thumb']:
|
||||||
|
@ -354,13 +361,13 @@ class Libraries(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
dict = {'recordsFiltered': query['filteredCount'],
|
dict = {'recordsFiltered': query['filteredCount'],
|
||||||
'recordsTotal': query['totalCount'],
|
'recordsTotal': query['totalCount'],
|
||||||
'data': session.mask_session_info(rows),
|
'data': session.mask_session_info(rows),
|
||||||
'draw': query['draw']
|
'draw': query['draw']
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict
|
return dict
|
||||||
|
|
||||||
def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False, kwargs=None):
|
def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False, kwargs=None):
|
||||||
|
@ -372,7 +379,7 @@ class Libraries(object):
|
||||||
|
|
||||||
if not session.allow_session_library(section_id):
|
if not session.allow_session_library(section_id):
|
||||||
return default_return
|
return default_return
|
||||||
|
|
||||||
if section_id and not str(section_id).isdigit():
|
if section_id and not str(section_id).isdigit():
|
||||||
logger.warn("Tautulli Libraries :: Datatable media info called but invalid section_id provided.")
|
logger.warn("Tautulli Libraries :: Datatable media info called but invalid section_id provided.")
|
||||||
return default_return
|
return default_return
|
||||||
|
@ -466,7 +473,7 @@ class Libraries(object):
|
||||||
else:
|
else:
|
||||||
logger.warn("Tautulli Libraries :: Unable to get a list of library items.")
|
logger.warn("Tautulli Libraries :: Unable to get a list of library items.")
|
||||||
return default_return
|
return default_return
|
||||||
|
|
||||||
new_rows = []
|
new_rows = []
|
||||||
for item in children_list:
|
for item in children_list:
|
||||||
## TODO: Check list of media info items, currently only grabs first item
|
## TODO: Check list of media info items, currently only grabs first item
|
||||||
|
@ -529,8 +536,8 @@ class Libraries(object):
|
||||||
item['play_count'] = None
|
item['play_count'] = None
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# Get datatables JSON data
|
# Get datatables JSON data
|
||||||
if kwargs.get('json_data'):
|
if kwargs.get('json_data'):
|
||||||
json_data = helpers.process_json_kwargs(json_kwargs=kwargs.get('json_data'))
|
json_data = helpers.process_json_kwargs(json_kwargs=kwargs.get('json_data'))
|
||||||
#print json_data
|
#print json_data
|
||||||
|
@ -540,7 +547,7 @@ class Libraries(object):
|
||||||
if search_value:
|
if search_value:
|
||||||
searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] + ['title']
|
searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] + ['title']
|
||||||
for row in rows:
|
for row in rows:
|
||||||
for k,v in row.iteritems():
|
for k,v in row.items():
|
||||||
if k in searchable_columns and search_value in v.lower():
|
if k in searchable_columns and search_value in v.lower():
|
||||||
results.append(row)
|
results.append(row)
|
||||||
break
|
break
|
||||||
|
@ -578,13 +585,13 @@ class Libraries(object):
|
||||||
'filtered_file_size': filtered_file_size,
|
'filtered_file_size': filtered_file_size,
|
||||||
'total_file_size': total_file_size
|
'total_file_size': total_file_size
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict
|
return dict
|
||||||
|
|
||||||
def get_media_info_file_sizes(self, section_id=None, rating_key=None):
|
def get_media_info_file_sizes(self, section_id=None, rating_key=None):
|
||||||
if not session.allow_session_library(section_id):
|
if not session.allow_session_library(section_id):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if section_id and not str(section_id).isdigit():
|
if section_id and not str(section_id).isdigit():
|
||||||
logger.warn("Tautulli Libraries :: Datatable media info file size called but invalid section_id provided.")
|
logger.warn("Tautulli Libraries :: Datatable media info file size called but invalid section_id provided.")
|
||||||
return False
|
return False
|
||||||
|
@ -629,7 +636,7 @@ class Libraries(object):
|
||||||
for item in rows:
|
for item in rows:
|
||||||
if item['rating_key'] and not item['file_size']:
|
if item['rating_key'] and not item['file_size']:
|
||||||
file_size = 0
|
file_size = 0
|
||||||
|
|
||||||
metadata = pms_connect.get_metadata_children_details(rating_key=item['rating_key'],
|
metadata = pms_connect.get_metadata_children_details(rating_key=item['rating_key'],
|
||||||
get_children=True)
|
get_children=True)
|
||||||
|
|
||||||
|
@ -669,7 +676,7 @@ class Libraries(object):
|
||||||
logger.debug("Tautulli Libraries :: File sizes updated for section_id %s." % section_id)
|
logger.debug("Tautulli Libraries :: File sizes updated for section_id %s." % section_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_config(self, section_id=None, custom_thumb='', do_notify=1, keep_history=1, do_notify_created=1):
|
def set_config(self, section_id=None, custom_thumb='', do_notify=1, keep_history=1, do_notify_created=1):
|
||||||
if section_id:
|
if section_id:
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
@ -759,7 +766,7 @@ class Libraries(object):
|
||||||
|
|
||||||
if library_details:
|
if library_details:
|
||||||
return library_details
|
return library_details
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warn("Tautulli Users :: Unable to retrieve library %s from database. Returning 'Local' library."
|
logger.warn("Tautulli Users :: Unable to retrieve library %s from database. Returning 'Local' library."
|
||||||
% section_id)
|
% section_id)
|
||||||
|
@ -856,7 +863,7 @@ class Libraries(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("Tautulli Libraries :: Unable to execute database query for get_user_stats: %s." % e)
|
logger.warn("Tautulli Libraries :: Unable to execute database query for get_user_stats: %s." % e)
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
row = {'friendly_name': item['friendly_name'],
|
row = {'friendly_name': item['friendly_name'],
|
||||||
'user_id': item['user_id'],
|
'user_id': item['user_id'],
|
||||||
|
@ -864,7 +871,7 @@ class Libraries(object):
|
||||||
'total_plays': item['user_count']
|
'total_plays': item['user_count']
|
||||||
}
|
}
|
||||||
user_stats.append(row)
|
user_stats.append(row)
|
||||||
|
|
||||||
return session.mask_session_info(user_stats, mask_metadata=False)
|
return session.mask_session_info(user_stats, mask_metadata=False)
|
||||||
|
|
||||||
def get_recently_watched(self, section_id=None, limit='10'):
|
def get_recently_watched(self, section_id=None, limit='10'):
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
"""
|
# -*- coding: utf-8 -*-
|
||||||
Locking-related classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
import plexpy.logger
|
# This file is part of Tautulli.
|
||||||
|
#
|
||||||
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Tautulli is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
|
import queue
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import Queue
|
|
||||||
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
class TimedLock(object):
|
class TimedLock(object):
|
||||||
|
@ -28,7 +45,7 @@ class TimedLock(object):
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.last_used = 0
|
self.last_used = 0
|
||||||
self.minimum_delta = minimum_delta
|
self.minimum_delta = minimum_delta
|
||||||
self.queue = Queue.Queue()
|
self.queue = queue.Queue()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -39,14 +56,14 @@ class TimedLock(object):
|
||||||
sleep_amount = self.minimum_delta - delta
|
sleep_amount = self.minimum_delta - delta
|
||||||
if sleep_amount >= 0:
|
if sleep_amount >= 0:
|
||||||
# zero sleeps give the cpu a chance to task-switch
|
# zero sleeps give the cpu a chance to task-switch
|
||||||
plexpy.logger.debug('Sleeping %s (interval)', sleep_amount)
|
logger.debug('Sleeping %s (interval)', sleep_amount)
|
||||||
time.sleep(sleep_amount)
|
time.sleep(sleep_amount)
|
||||||
while not self.queue.empty():
|
while not self.queue.empty():
|
||||||
try:
|
try:
|
||||||
seconds = self.queue.get(False)
|
seconds = self.queue.get(False)
|
||||||
plexpy.logger.debug('Sleeping %s (queued)', seconds)
|
logger.debug('Sleeping %s (queued)', seconds)
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
except Queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
|
|
||||||
|
@ -65,7 +82,7 @@ class TimedLock(object):
|
||||||
"""
|
"""
|
||||||
# We use a queue so that we don't have to synchronize
|
# We use a queue so that we don't have to synchronize
|
||||||
# across threads and with or without locks
|
# across threads and with or without locks
|
||||||
plexpy.logger.info('Adding %s to queue', seconds)
|
logger.info('Adding %s to queue', seconds)
|
||||||
self.queue.put(seconds)
|
self.queue.put(seconds)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -13,11 +15,15 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
def get_log_tail(window=20, parsed=True, log_type="server"):
|
def get_log_tail(window=20, parsed=True, log_type="server"):
|
||||||
|
|
||||||
|
@ -45,7 +51,7 @@ def get_log_tail(window=20, parsed=True, log_type="server"):
|
||||||
try:
|
try:
|
||||||
log_time = i.split(' [')[0]
|
log_time = i.split(' [')[0]
|
||||||
log_level = i.split('] ', 1)[1].split(' - ', 1)[0]
|
log_level = i.split('] ', 1)[1].split(' - ', 1)[0]
|
||||||
log_msg = unicode(i.split('] ', 1)[1].split(' - ', 1)[1], 'utf-8')
|
log_msg = str(i.split('] ', 1)[1].split(' - ', 1)[1], 'utf-8')
|
||||||
full_line = [log_time, log_level, log_msg]
|
full_line = [log_time, log_level, log_msg]
|
||||||
clean_lines.append(full_line)
|
clean_lines.append(full_line)
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -13,6 +15,10 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
from past.builtins import basestring
|
||||||
|
|
||||||
from logutils.queue import QueueHandler, QueueListener
|
from logutils.queue import QueueHandler, QueueListener
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
|
|
||||||
|
@ -27,9 +33,10 @@ import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import helpers
|
from plexpy.helpers import is_public_ip
|
||||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||||
|
|
||||||
|
|
||||||
# These settings are for file logging only
|
# These settings are for file logging only
|
||||||
FILENAME = "tautulli.log"
|
FILENAME = "tautulli.log"
|
||||||
FILENAME_API = "tautulli_api.log"
|
FILENAME_API = "tautulli_api.log"
|
||||||
|
@ -54,7 +61,7 @@ def blacklist_config(config):
|
||||||
blacklist = set()
|
blacklist = set()
|
||||||
blacklist_keys = ['HOOK', 'APIKEY', 'KEY', 'PASSWORD', 'TOKEN']
|
blacklist_keys = ['HOOK', 'APIKEY', 'KEY', 'PASSWORD', 'TOKEN']
|
||||||
|
|
||||||
for key, value in config.iteritems():
|
for key, value in config.items():
|
||||||
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
||||||
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
|
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
|
||||||
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
|
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
|
||||||
|
@ -113,14 +120,14 @@ class PublicIPFilter(logging.Filter):
|
||||||
# Currently only checking for ipv4 addresses
|
# Currently only checking for ipv4 addresses
|
||||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg)
|
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg)
|
||||||
for ip in ipv4:
|
for ip in ipv4:
|
||||||
if helpers.is_public_ip(ip):
|
if is_public_ip(ip):
|
||||||
record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
||||||
|
|
||||||
args = []
|
args = []
|
||||||
for arg in record.args:
|
for arg in record.args:
|
||||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', arg) if isinstance(arg, basestring) else []
|
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', arg) if isinstance(arg, basestring) else []
|
||||||
for ip in ipv4:
|
for ip in ipv4:
|
||||||
if helpers.is_public_ip(ip):
|
if is_public_ip(ip):
|
||||||
arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
record.args = tuple(args)
|
record.args = tuple(args)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,12 +15,13 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
from plexpy import database
|
||||||
import database
|
from plexpy import logger
|
||||||
import helpers
|
|
||||||
import logger
|
|
||||||
|
|
||||||
|
|
||||||
TEMP_DEVICE_TOKEN = None
|
TEMP_DEVICE_TOKEN = None
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,6 +15,8 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -20,9 +24,9 @@ from apscheduler.triggers.cron import CronTrigger
|
||||||
import email.utils
|
import email.utils
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import database
|
from plexpy import database
|
||||||
import logger
|
from plexpy import logger
|
||||||
import newsletters
|
from plexpy import newsletters
|
||||||
|
|
||||||
|
|
||||||
NEWSLETTER_SCHED = None
|
NEWSLETTER_SCHED = None
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,6 +15,11 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import json
|
import json
|
||||||
|
@ -23,14 +30,14 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import logger
|
from plexpy import logger
|
||||||
import newsletter_handler
|
from plexpy import newsletter_handler
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
from notifiers import send_notification, EMAIL
|
from plexpy.notifiers import send_notification, EMAIL
|
||||||
|
|
||||||
|
|
||||||
AGENT_IDS = {
|
AGENT_IDS = {
|
||||||
|
@ -229,11 +236,11 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
||||||
email_config_prefix = 'newsletter_email_'
|
email_config_prefix = 'newsletter_email_'
|
||||||
|
|
||||||
newsletter_config = {k[len(config_prefix):]: kwargs.pop(k)
|
newsletter_config = {k[len(config_prefix):]: kwargs.pop(k)
|
||||||
for k in kwargs.keys() if k.startswith(config_prefix)}
|
for k in list(kwargs.keys()) if k.startswith(config_prefix)}
|
||||||
email_config = {k[len(email_config_prefix):]: kwargs.pop(k)
|
email_config = {k[len(email_config_prefix):]: kwargs.pop(k)
|
||||||
for k in kwargs.keys() if k.startswith(email_config_prefix)}
|
for k in list(kwargs.keys()) if k.startswith(email_config_prefix)}
|
||||||
|
|
||||||
for cfg, val in email_config.iteritems():
|
for cfg, val in email_config.items():
|
||||||
# Check for a password config keys and a blank password from the HTML form
|
# Check for a password config keys and a blank password from the HTML form
|
||||||
if 'password' in cfg and val == ' ':
|
if 'password' in cfg and val == ' ':
|
||||||
# Get the previous password so we don't overwrite it with a blank value
|
# Get the previous password so we don't overwrite it with a blank value
|
||||||
|
@ -418,7 +425,7 @@ class Newsletter(object):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
new_config = {}
|
new_config = {}
|
||||||
for k, v in default.iteritems():
|
for k, v in default.items():
|
||||||
if isinstance(v, int):
|
if isinstance(v, int):
|
||||||
new_config[k] = helpers.cast_to_int(config.get(k, v))
|
new_config[k] = helpers.cast_to_int(config.get(k, v))
|
||||||
elif isinstance(v, list):
|
elif isinstance(v, list):
|
||||||
|
@ -602,50 +609,50 @@ class Newsletter(object):
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
def build_text(self):
|
def build_text(self):
|
||||||
from notification_handler import CustomFormatter
|
from plexpy.notification_handler import CustomFormatter
|
||||||
custom_formatter = CustomFormatter()
|
custom_formatter = CustomFormatter()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subject = custom_formatter.format(unicode(self.subject), **self.parameters)
|
subject = custom_formatter.format(str(self.subject), **self.parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter subject. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter subject. Using fallback." % e)
|
||||||
subject = unicode(self._DEFAULT_SUBJECT).format(**self.parameters)
|
subject = str(self._DEFAULT_SUBJECT).format(**self.parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
|
||||||
subject = unicode(self._DEFAULT_SUBJECT).format(**self.parameters)
|
subject = str(self._DEFAULT_SUBJECT).format(**self.parameters)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body = custom_formatter.format(unicode(self.body), **self.parameters)
|
body = custom_formatter.format(str(self.body), **self.parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter body. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter body. Using fallback." % e)
|
||||||
body = unicode(self._DEFAULT_BODY).format(**self.parameters)
|
body = str(self._DEFAULT_BODY).format(**self.parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter body: %s. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter body: %s. Using fallback." % e)
|
||||||
body = unicode(self._DEFAULT_BODY).format(**self.parameters)
|
body = str(self._DEFAULT_BODY).format(**self.parameters)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = custom_formatter.format(unicode(self.message), **self.parameters)
|
message = custom_formatter.format(str(self.message), **self.parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter message. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter message. Using fallback." % e)
|
||||||
message = unicode(self._DEFAULT_MESSAGE).format(**self.parameters)
|
message = str(self._DEFAULT_MESSAGE).format(**self.parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter message: %s. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter message: %s. Using fallback." % e)
|
||||||
message = unicode(self._DEFAULT_MESSAGE).format(**self.parameters)
|
message = str(self._DEFAULT_MESSAGE).format(**self.parameters)
|
||||||
|
|
||||||
return subject, body, message
|
return subject, body, message
|
||||||
|
|
||||||
def build_filename(self):
|
def build_filename(self):
|
||||||
from notification_handler import CustomFormatter
|
from plexpy.notification_handler import CustomFormatter
|
||||||
custom_formatter = CustomFormatter()
|
custom_formatter = CustomFormatter()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename = custom_formatter.format(unicode(self.filename), **self.parameters)
|
filename = custom_formatter.format(str(self.filename), **self.parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse parameter %s in newsletter filename. Using fallback." % e)
|
||||||
filename = unicode(self._DEFAULT_FILENAME).format(**self.parameters)
|
filename = str(self._DEFAULT_FILENAME).format(**self.parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
|
logger.error("Tautulli Newsletter :: Unable to parse custom newsletter subject: %s. Using fallback." % e)
|
||||||
filename = unicode(self._DEFAULT_FILENAME).format(**self.parameters)
|
filename = str(self._DEFAULT_FILENAME).format(**self.parameters)
|
||||||
|
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
@ -682,7 +689,7 @@ class RecentlyAdded(Newsletter):
|
||||||
_TEMPLATE = 'recently_added.html'
|
_TEMPLATE = 'recently_added.html'
|
||||||
|
|
||||||
def _get_recently_added(self, media_type=None):
|
def _get_recently_added(self, media_type=None):
|
||||||
from notification_handler import format_group_index
|
from plexpy.notification_handler import format_group_index
|
||||||
|
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
|
||||||
|
@ -798,7 +805,7 @@ class RecentlyAdded(Newsletter):
|
||||||
return recently_added
|
return recently_added
|
||||||
|
|
||||||
def retrieve_data(self):
|
def retrieve_data(self):
|
||||||
from notification_handler import get_img_info, set_hash_image_info
|
from plexpy.notification_handler import get_img_info, set_hash_image_info
|
||||||
|
|
||||||
if not self.config['incl_libraries']:
|
if not self.config['incl_libraries']:
|
||||||
logger.warn("Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME)
|
logger.warn("Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -14,6 +16,17 @@
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import next
|
||||||
|
from builtins import map
|
||||||
|
from builtins import str
|
||||||
|
from builtins import range
|
||||||
|
from past.builtins import basestring
|
||||||
|
from past.utils import old_div
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import bleach
|
import bleach
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
|
@ -31,19 +44,16 @@ import time
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import datafactory
|
from plexpy import datafactory
|
||||||
import libraries
|
from plexpy import logger
|
||||||
import logger
|
from plexpy import helpers
|
||||||
import helpers
|
from plexpy import notifiers
|
||||||
import notifiers
|
from plexpy import pmsconnect
|
||||||
import plextv
|
from plexpy import request
|
||||||
import pmsconnect
|
from plexpy.newsletter_handler import notify as notify_newsletter
|
||||||
import request
|
|
||||||
import users
|
|
||||||
from newsletter_handler import notify as notify_newsletter
|
|
||||||
|
|
||||||
|
|
||||||
def process_queue():
|
def process_queue():
|
||||||
|
@ -63,7 +73,7 @@ def process_queue():
|
||||||
add_notifier_each(**params)
|
add_notifier_each(**params)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Tautulli NotificationHandler :: Notification thread exception: %s" % e)
|
logger.exception("Tautulli NotificationHandler :: Notification thread exception: %s" % e)
|
||||||
|
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
|
|
||||||
logger.info("Tautulli NotificationHandler :: Notification thread exiting...")
|
logger.info("Tautulli NotificationHandler :: Notification thread exiting...")
|
||||||
|
@ -174,12 +184,12 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||||
|
|
||||||
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
|
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
|
||||||
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
||||||
|
|
||||||
if notify_action == 'on_stop':
|
if notify_action == 'on_stop':
|
||||||
return (plexpy.CONFIG.NOTIFY_CONSECUTIVE or
|
return (plexpy.CONFIG.NOTIFY_CONSECUTIVE or
|
||||||
(stream_data['media_type'] == 'movie' and progress_percent < plexpy.CONFIG.MOVIE_WATCHED_PERCENT) or
|
(stream_data['media_type'] == 'movie' and progress_percent < plexpy.CONFIG.MOVIE_WATCHED_PERCENT) or
|
||||||
(stream_data['media_type'] == 'episode' and progress_percent < plexpy.CONFIG.TV_WATCHED_PERCENT))
|
(stream_data['media_type'] == 'episode' and progress_percent < plexpy.CONFIG.TV_WATCHED_PERCENT))
|
||||||
|
|
||||||
elif notify_action == 'on_resume':
|
elif notify_action == 'on_resume':
|
||||||
return plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
|
return plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
|
||||||
|
|
||||||
|
@ -254,7 +264,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||||
# Cast the condition values to the correct type
|
# Cast the condition values to the correct type
|
||||||
try:
|
try:
|
||||||
if parameter_type == 'str':
|
if parameter_type == 'str':
|
||||||
values = [unicode(v).lower() for v in values]
|
values = [str(v).lower() for v in values]
|
||||||
|
|
||||||
elif parameter_type == 'int':
|
elif parameter_type == 'int':
|
||||||
values = [helpers.cast_to_int(v) for v in values]
|
values = [helpers.cast_to_int(v) for v in values]
|
||||||
|
@ -270,7 +280,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
||||||
# Cast the parameter value to the correct type
|
# Cast the parameter value to the correct type
|
||||||
try:
|
try:
|
||||||
if parameter_type == 'str':
|
if parameter_type == 'str':
|
||||||
parameter_value = unicode(parameter_value).lower()
|
parameter_value = str(parameter_value).lower()
|
||||||
|
|
||||||
elif parameter_type == 'int':
|
elif parameter_type == 'int':
|
||||||
parameter_value = helpers.cast_to_int(parameter_value)
|
parameter_value = helpers.cast_to_int(parameter_value)
|
||||||
|
@ -540,9 +550,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||||
transcode_decision = 'Direct Play'
|
transcode_decision = 'Direct Play'
|
||||||
|
|
||||||
if notify_action != 'on_play':
|
if notify_action != 'on_play':
|
||||||
stream_duration = int((time.time() -
|
stream_duration = int(old_div((time.time() -
|
||||||
helpers.cast_to_int(session.get('started', 0)) -
|
helpers.cast_to_int(session.get('started', 0)) -
|
||||||
helpers.cast_to_int(session.get('paused_counter', 0))) / 60)
|
helpers.cast_to_int(session.get('paused_counter', 0))), 60))
|
||||||
else:
|
else:
|
||||||
stream_duration = 0
|
stream_duration = 0
|
||||||
|
|
||||||
|
@ -1137,19 +1147,19 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||||
subject = str_formatter(subject)
|
subject = str_formatter(subject)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
||||||
subject = unicode(default_subject).format(**parameters)
|
subject = str(default_subject).format(**parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
logger.error("Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
||||||
subject = unicode(default_subject).format(**parameters)
|
subject = str(default_subject).format(**parameters)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body = str_formatter(body)
|
body = str_formatter(body)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
|
logger.error("Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
|
||||||
body = unicode(default_body).format(**parameters)
|
body = str(default_body).format(**parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
logger.error("Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
||||||
body = unicode(default_body).format(**parameters)
|
body = str(default_body).format(**parameters)
|
||||||
|
|
||||||
return subject, body, script_args
|
return subject, body, script_args
|
||||||
|
|
||||||
|
@ -1165,7 +1175,7 @@ def strip_tag(data, agent_id=None):
|
||||||
'u': [],
|
'u': [],
|
||||||
'a': ['href'],
|
'a': ['href'],
|
||||||
'font': ['color']}
|
'font': ['color']}
|
||||||
data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
data = bleach.clean(data, tags=list(whitelist.keys()), attributes=whitelist, strip=True)
|
||||||
|
|
||||||
elif agent_id in (10, 14, 20):
|
elif agent_id in (10, 14, 20):
|
||||||
# Don't remove tags for Email, Slack, and Discord
|
# Don't remove tags for Email, Slack, and Discord
|
||||||
|
@ -1178,11 +1188,11 @@ def strip_tag(data, agent_id=None):
|
||||||
'code': [],
|
'code': [],
|
||||||
'pre': [],
|
'pre': [],
|
||||||
'a': ['href']}
|
'a': ['href']}
|
||||||
data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
data = bleach.clean(data, tags=list(whitelist.keys()), attributes=whitelist, strip=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
whitelist = {}
|
whitelist = {}
|
||||||
data = bleach.clean(data, tags=whitelist.keys(), attributes=whitelist, strip=True)
|
data = bleach.clean(data, tags=list(whitelist.keys()), attributes=whitelist, strip=True)
|
||||||
|
|
||||||
# Resubstitute temporary tokens for < and > in parameter prefix and suffix
|
# Resubstitute temporary tokens for < and > in parameter prefix and suffix
|
||||||
return data.replace('%temp_lt_token%', '<').replace('%temp_gt_token%', '>')
|
return data.replace('%temp_lt_token%', '<').replace('%temp_gt_token%', '>')
|
||||||
|
@ -1194,8 +1204,8 @@ def format_group_index(group_keys):
|
||||||
num = []
|
num = []
|
||||||
num00 = []
|
num00 = []
|
||||||
|
|
||||||
for k, g in groupby(enumerate(group_keys), lambda (i, x): i-x):
|
for k, g in groupby(enumerate(group_keys), lambda i_x: i_x[0]-i_x[1]):
|
||||||
group = map(itemgetter(1), g)
|
group = list(map(itemgetter(1), g))
|
||||||
g_min, g_max = min(group), max(group)
|
g_min, g_max = min(group), max(group)
|
||||||
|
|
||||||
if g_min == g_max:
|
if g_min == g_max:
|
||||||
|
@ -1382,7 +1392,7 @@ def lookup_tvmaze_by_id(rating_key=None, thetvdb_id=None, imdb_id=None):
|
||||||
imdb_id = tvmaze_json.get('externals', {}).get('imdb', '')
|
imdb_id = tvmaze_json.get('externals', {}).get('imdb', '')
|
||||||
tvmaze_id = tvmaze_json.get('id', '')
|
tvmaze_id = tvmaze_json.get('id', '')
|
||||||
tvmaze_url = tvmaze_json.get('url', '')
|
tvmaze_url = tvmaze_json.get('url', '')
|
||||||
|
|
||||||
keys = {'tvmaze_id': tvmaze_id}
|
keys = {'tvmaze_id': tvmaze_id}
|
||||||
tvmaze_info = {'rating_key': rating_key,
|
tvmaze_info = {'rating_key': rating_key,
|
||||||
'thetvdb_id': thetvdb_id,
|
'thetvdb_id': thetvdb_id,
|
||||||
|
@ -1586,7 +1596,7 @@ def lookup_musicbrainz_info(musicbrainz_type=None, rating_key=None, artist=None,
|
||||||
def str_format(s, parameters):
|
def str_format(s, parameters):
|
||||||
custom_formatter = CustomFormatter()
|
custom_formatter = CustomFormatter()
|
||||||
if isinstance(s, basestring):
|
if isinstance(s, basestring):
|
||||||
return custom_formatter.format(unicode(s), **parameters)
|
return custom_formatter.format(str(s), **parameters)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
@ -1602,11 +1612,11 @@ class CustomFormatter(Formatter):
|
||||||
elif conversion == 'r':
|
elif conversion == 'r':
|
||||||
return repr(value)
|
return repr(value)
|
||||||
elif conversion == 'u': # uppercase
|
elif conversion == 'u': # uppercase
|
||||||
return unicode(value).upper()
|
return str(value).upper()
|
||||||
elif conversion == 'l': # lowercase
|
elif conversion == 'l': # lowercase
|
||||||
return unicode(value).lower()
|
return str(value).lower()
|
||||||
elif conversion == 'c': # capitalize
|
elif conversion == 'c': # capitalize
|
||||||
return unicode(value).title()
|
return str(value).title()
|
||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -1616,7 +1626,7 @@ class CustomFormatter(Formatter):
|
||||||
match = re.match(pattern, format_spec)
|
match = re.match(pattern, format_spec)
|
||||||
if value and match:
|
if value and match:
|
||||||
groups = match.groupdict()
|
groups = match.groupdict()
|
||||||
items = [x.strip() for x in unicode(value).split(',')]
|
items = [x.strip() for x in str(value).split(',')]
|
||||||
start = groups['start'] or None
|
start = groups['start'] or None
|
||||||
end = groups['end'] or None
|
end = groups['end'] or None
|
||||||
if start is not None:
|
if start is not None:
|
||||||
|
@ -1666,7 +1676,7 @@ class CustomFormatter(Formatter):
|
||||||
|
|
||||||
if prefix or suffix:
|
if prefix or suffix:
|
||||||
real_format_string = '{' + real_format_string + '}'
|
real_format_string = '{' + real_format_string + '}'
|
||||||
_, field_name, format_spec, conversion, _, _ = self.parse(real_format_string).next()
|
_, field_name, format_spec, conversion, _, _ = next(self.parse(real_format_string))
|
||||||
|
|
||||||
yield literal_text, field_name, format_spec, conversion, prefix, suffix
|
yield literal_text, field_name, format_spec, conversion, prefix, suffix
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,6 +15,13 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import bleach
|
import bleach
|
||||||
import json
|
import json
|
||||||
|
@ -28,8 +37,8 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from urllib import urlencode
|
from urllib.parse import urlencode
|
||||||
from urlparse import urlparse
|
from urllib.parse import urlparse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -54,14 +63,14 @@ import twitter
|
||||||
import pynma
|
import pynma
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
import mobile_app
|
from plexpy import mobile_app
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import request
|
from plexpy import request
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
BROWSER_NOTIFIERS = {}
|
BROWSER_NOTIFIERS = {}
|
||||||
|
@ -438,7 +447,7 @@ def get_notifiers(notifier_id=None, notify_action=None):
|
||||||
% (', '.join(notify_actions), where), args=args)
|
% (', '.join(notify_actions), where), args=args)
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
item['active'] = int(any([item.pop(k) for k in item.keys() if k in notify_actions]))
|
item['active'] = int(any([item.pop(k) for k in list(item.keys()) if k in notify_actions]))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -483,7 +492,7 @@ def get_notifier_config(notifier_id=None, mask_passwords=False):
|
||||||
|
|
||||||
notifier_actions = {}
|
notifier_actions = {}
|
||||||
notifier_text = {}
|
notifier_text = {}
|
||||||
for k in result.keys():
|
for k in list(result.keys()):
|
||||||
if k in notify_actions:
|
if k in notify_actions:
|
||||||
subject = result.pop(k + '_subject')
|
subject = result.pop(k + '_subject')
|
||||||
body = result.pop(k + '_body')
|
body = result.pop(k + '_body')
|
||||||
|
@ -581,15 +590,15 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
||||||
config_prefix = agent['name'] + '_'
|
config_prefix = agent['name'] + '_'
|
||||||
|
|
||||||
actions = {k: helpers.cast_to_int(kwargs.pop(k))
|
actions = {k: helpers.cast_to_int(kwargs.pop(k))
|
||||||
for k in kwargs.keys() if k in notify_actions}
|
for k in list(kwargs.keys()) if k in notify_actions}
|
||||||
subject_text = {k: kwargs.pop(k)
|
subject_text = {k: kwargs.pop(k)
|
||||||
for k in kwargs.keys() if k.startswith(notify_actions) and k.endswith('_subject')}
|
for k in list(kwargs.keys()) if k.startswith(notify_actions) and k.endswith('_subject')}
|
||||||
body_text = {k: kwargs.pop(k)
|
body_text = {k: kwargs.pop(k)
|
||||||
for k in kwargs.keys() if k.startswith(notify_actions) and k.endswith('_body')}
|
for k in list(kwargs.keys()) if k.startswith(notify_actions) and k.endswith('_body')}
|
||||||
notifier_config = {k[len(config_prefix):]: kwargs.pop(k)
|
notifier_config = {k[len(config_prefix):]: kwargs.pop(k)
|
||||||
for k in kwargs.keys() if k.startswith(config_prefix)}
|
for k in list(kwargs.keys()) if k.startswith(config_prefix)}
|
||||||
|
|
||||||
for cfg, val in notifier_config.iteritems():
|
for cfg, val in notifier_config.items():
|
||||||
# Check for a password config keys and a blank password from the HTML form
|
# Check for a password config keys and a blank password from the HTML form
|
||||||
if 'password' in cfg and val == ' ':
|
if 'password' in cfg and val == ' ':
|
||||||
# Get the previous password so we don't overwrite it with a blank value
|
# Get the previous password so we don't overwrite it with a blank value
|
||||||
|
@ -793,7 +802,7 @@ class Notifier(object):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
new_config = {}
|
new_config = {}
|
||||||
for k, v in default.iteritems():
|
for k, v in default.items():
|
||||||
if isinstance(v, int):
|
if isinstance(v, int):
|
||||||
new_config[k] = helpers.cast_to_int(config.get(k, v))
|
new_config[k] = helpers.cast_to_int(config.get(k, v))
|
||||||
elif isinstance(v, list):
|
elif isinstance(v, list):
|
||||||
|
@ -1404,9 +1413,9 @@ class EMAIL(Notifier):
|
||||||
user_emails_cc.update(emails)
|
user_emails_cc.update(emails)
|
||||||
user_emails_bcc.update(emails)
|
user_emails_bcc.update(emails)
|
||||||
|
|
||||||
user_emails_to = [{'value': k, 'text': v} for k, v in user_emails_to.iteritems()]
|
user_emails_to = [{'value': k, 'text': v} for k, v in user_emails_to.items()]
|
||||||
user_emails_cc = [{'value': k, 'text': v} for k, v in user_emails_cc.iteritems()]
|
user_emails_cc = [{'value': k, 'text': v} for k, v in user_emails_cc.items()]
|
||||||
user_emails_bcc = [{'value': k, 'text': v} for k, v in user_emails_bcc.iteritems()]
|
user_emails_bcc = [{'value': k, 'text': v} for k, v in user_emails_bcc.items()]
|
||||||
|
|
||||||
return user_emails_to, user_emails_cc, user_emails_bcc
|
return user_emails_to, user_emails_cc, user_emails_bcc
|
||||||
|
|
||||||
|
@ -2019,7 +2028,7 @@ class IFTTT(Notifier):
|
||||||
}
|
}
|
||||||
|
|
||||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||||
event = unicode(self.config['event']).format(action=action)
|
event = str(self.config['event']).format(action=action)
|
||||||
|
|
||||||
data = {'value1': subject.encode('utf-8'),
|
data = {'value1': subject.encode('utf-8'),
|
||||||
'value2': body.encode('utf-8')}
|
'value2': body.encode('utf-8')}
|
||||||
|
@ -3043,7 +3052,7 @@ class SCRIPTS(Notifier):
|
||||||
for root, dirs, files in os.walk(scriptdir):
|
for root, dirs, files in os.walk(scriptdir):
|
||||||
for f in files:
|
for f in files:
|
||||||
name, ext = os.path.splitext(f)
|
name, ext = os.path.splitext(f)
|
||||||
if ext in self.script_exts.keys():
|
if ext in list(self.script_exts.keys()):
|
||||||
rfp = os.path.join(os.path.relpath(root, scriptdir), f)
|
rfp = os.path.join(os.path.relpath(root, scriptdir), f)
|
||||||
fp = os.path.join(root, f)
|
fp = os.path.join(root, f)
|
||||||
scripts[fp] = rfp
|
scripts[fp] = rfp
|
||||||
|
@ -3187,7 +3196,7 @@ class SCRIPTS(Notifier):
|
||||||
def _return_config_options(self):
|
def _return_config_options(self):
|
||||||
config_option = [{'label': 'Supported File Types',
|
config_option = [{'label': 'Supported File Types',
|
||||||
'description': '<span class="inline-pre">' + \
|
'description': '<span class="inline-pre">' + \
|
||||||
', '.join(self.script_exts.keys()) + '</span>',
|
', '.join(list(self.script_exts.keys())) + '</span>',
|
||||||
'input_type': 'help'
|
'input_type': 'help'
|
||||||
},
|
},
|
||||||
{'label': 'Script Folder',
|
{'label': 'Script Folder',
|
||||||
|
@ -3947,7 +3956,7 @@ def upgrade_config_to_db():
|
||||||
|
|
||||||
# Update the new config with the old config values
|
# Update the new config with the old config values
|
||||||
notifier_config = {}
|
notifier_config = {}
|
||||||
for conf, val in notifier_default_config.iteritems():
|
for conf, val in notifier_default_config.items():
|
||||||
c_key = agent_config_key + '_' + config_key_overrides.get(agent, {}).get(conf, conf)
|
c_key = agent_config_key + '_' + config_key_overrides.get(agent, {}).get(conf, conf)
|
||||||
notifier_config[agent + '_' + conf] = agent_config.get(c_key, val)
|
notifier_config[agent + '_' + conf] = agent_config.get(c_key, val)
|
||||||
|
|
||||||
|
@ -3964,15 +3973,15 @@ def upgrade_config_to_db():
|
||||||
|
|
||||||
# Reverse the dict to {script: [actions]}
|
# Reverse the dict to {script: [actions]}
|
||||||
script_actions = {}
|
script_actions = {}
|
||||||
for k, v in action_scripts.items():
|
for k, v in list(action_scripts.items()):
|
||||||
if v: script_actions.setdefault(v, set()).add(k)
|
if v: script_actions.setdefault(v, set()).add(k)
|
||||||
|
|
||||||
# Add a new script notifier for each script if the action was enabled
|
# Add a new script notifier for each script if the action was enabled
|
||||||
for script, actions in script_actions.items():
|
for script, actions in list(script_actions.items()):
|
||||||
if any(agent_actions[a] for a in actions):
|
if any(agent_actions[a] for a in actions):
|
||||||
temp_config = notifier_config
|
temp_config = notifier_config
|
||||||
temp_config.update({a: 0 for a in agent_actions.keys()})
|
temp_config.update({a: 0 for a in list(agent_actions.keys())})
|
||||||
temp_config.update({a + '_subject': '' for a in agent_actions.keys()})
|
temp_config.update({a + '_subject': '' for a in list(agent_actions.keys())})
|
||||||
for a in actions:
|
for a in actions:
|
||||||
if agent_actions[a]:
|
if agent_actions[a]:
|
||||||
temp_config[a] = agent_actions[a]
|
temp_config[a] = agent_actions[a]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,17 +15,20 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_pinger
|
from plexpy import activity_pinger
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
def extract_plexivity_xml(xml=None):
|
def extract_plexivity_xml(xml=None):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
|
@ -16,17 +15,22 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import http_handler
|
from plexpy import http_handler
|
||||||
import logger
|
from plexpy import logger
|
||||||
import users
|
from plexpy import users
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import session
|
from plexpy import session
|
||||||
|
|
||||||
|
|
||||||
def get_server_resources(return_presence=False, return_server=False, **kwargs):
|
def get_server_resources(return_presence=False, return_server=False, **kwargs):
|
||||||
|
@ -155,7 +159,7 @@ class PlexTV(object):
|
||||||
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
||||||
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
||||||
'Authorization': 'Basic %s' % base64string}
|
'Authorization': 'Basic %s' % base64string}
|
||||||
|
|
||||||
request = self.request_handler.make_request(uri=uri,
|
request = self.request_handler.make_request(uri=uri,
|
||||||
request_type='POST',
|
request_type='POST',
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
@ -199,7 +203,7 @@ class PlexTV(object):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
logger.warn("Tautulli PlexTV :: No existing Tautulli device found.")
|
logger.warn("Tautulli PlexTV :: No existing Tautulli device found.")
|
||||||
|
|
||||||
logger.info("Tautulli PlexTV :: Fetching a new Plex.tv token for Tautulli.")
|
logger.info("Tautulli PlexTV :: Fetching a new Plex.tv token for Tautulli.")
|
||||||
user = self.get_token()
|
user = self.get_token()
|
||||||
if user:
|
if user:
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,16 +15,19 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_pinger
|
from plexpy import activity_pinger
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import database
|
from plexpy import database
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import logger
|
from plexpy import logger
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
def extract_plexwatch_xml(xml=None):
|
def extract_plexwatch_xml(xml=None):
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,20 +15,27 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import common
|
from plexpy import common
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import http_handler
|
from plexpy import http_handler
|
||||||
import logger
|
from plexpy import logger
|
||||||
import plextv
|
from plexpy import plextv
|
||||||
import session
|
from plexpy import session
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
def get_server_friendly_name():
|
def get_server_friendly_name():
|
||||||
|
@ -101,7 +110,7 @@ class PmsConnect(object):
|
||||||
|
|
||||||
Output: array
|
Output: array
|
||||||
"""
|
"""
|
||||||
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, urllib.quote_plus(reason))
|
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, urllib.parse.quote_plus(reason))
|
||||||
request = self.request_handler.make_request(uri=uri,
|
request = self.request_handler.make_request(uri=uri,
|
||||||
request_type='GET',
|
request_type='GET',
|
||||||
output_format=output_format)
|
output_format=output_format)
|
||||||
|
@ -352,7 +361,7 @@ class PmsConnect(object):
|
||||||
|
|
||||||
Output: array
|
Output: array
|
||||||
"""
|
"""
|
||||||
uri = '/hubs/search?query=' + urllib.quote(query.encode('utf8')) + '&limit=' + limit + '&includeCollections=1'
|
uri = '/hubs/search?query=' + urllib.parse.quote(query.encode('utf8')) + '&limit=' + limit + '&includeCollections=1'
|
||||||
request = self.request_handler.make_request(uri=uri,
|
request = self.request_handler.make_request(uri=uri,
|
||||||
request_type='GET',
|
request_type='GET',
|
||||||
output_format=output_format)
|
output_format=output_format)
|
||||||
|
@ -726,7 +735,7 @@ class PmsConnect(object):
|
||||||
# Workaround for for duration sometimes reported in minutes for a show
|
# Workaround for for duration sometimes reported in minutes for a show
|
||||||
duration = helpers.get_xml_attr(metadata_main, 'duration')
|
duration = helpers.get_xml_attr(metadata_main, 'duration')
|
||||||
if duration.isdigit() and int(duration) < 1000:
|
if duration.isdigit() and int(duration) < 1000:
|
||||||
duration = unicode(int(duration) * 60 * 1000)
|
duration = str(int(duration) * 60 * 1000)
|
||||||
|
|
||||||
metadata = {'media_type': metadata_type,
|
metadata = {'media_type': metadata_type,
|
||||||
'section_id': section_id,
|
'section_id': section_id,
|
||||||
|
@ -1500,7 +1509,7 @@ class PmsConnect(object):
|
||||||
session_list.append(session_output)
|
session_list.append(session_output)
|
||||||
|
|
||||||
session_list = sorted(session_list, key=lambda k: k['session_key'])
|
session_list = sorted(session_list, key=lambda k: k['session_key'])
|
||||||
|
|
||||||
output = {'stream_count': helpers.get_xml_attr(xml_head[0], 'size'),
|
output = {'stream_count': helpers.get_xml_attr(xml_head[0], 'size'),
|
||||||
'sessions': session.mask_session_info(session_list)
|
'sessions': session.mask_session_info(session_list)
|
||||||
}
|
}
|
||||||
|
@ -1534,7 +1543,7 @@ class PmsConnect(object):
|
||||||
if not platform and helpers.get_xml_attr(player_info, 'product') == 'DLNA':
|
if not platform and helpers.get_xml_attr(player_info, 'product') == 'DLNA':
|
||||||
platform = 'DLNA'
|
platform = 'DLNA'
|
||||||
|
|
||||||
platform_name = next((v for k, v in common.PLATFORM_NAMES.iteritems() if k in platform.lower()), 'default')
|
platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default')
|
||||||
|
|
||||||
player_details = {'ip_address': helpers.get_xml_attr(player_info, 'address').split('::ffff:')[-1],
|
player_details = {'ip_address': helpers.get_xml_attr(player_info, 'address').split('::ffff:')[-1],
|
||||||
'ip_address_public': helpers.get_xml_attr(player_info, 'remotePublicAddress').split('::ffff:')[-1],
|
'ip_address_public': helpers.get_xml_attr(player_info, 'remotePublicAddress').split('::ffff:')[-1],
|
||||||
|
@ -2043,7 +2052,7 @@ class PmsConnect(object):
|
||||||
'user': user_details['username'], # Keep for backwards compatibility
|
'user': user_details['username'], # Keep for backwards compatibility
|
||||||
'channel_stream': channel_stream
|
'channel_stream': channel_stream
|
||||||
}
|
}
|
||||||
|
|
||||||
session_output.update(metadata_details)
|
session_output.update(metadata_details)
|
||||||
session_output.update(source_media_details)
|
session_output.update(source_media_details)
|
||||||
session_output.update(source_media_part_details)
|
session_output.update(source_media_part_details)
|
||||||
|
@ -2254,7 +2263,7 @@ class PmsConnect(object):
|
||||||
hub_identifier = helpers.get_xml_attr(h, 'hubIdentifier')
|
hub_identifier = helpers.get_xml_attr(h, 'hubIdentifier')
|
||||||
|
|
||||||
if size == '0' or not hub_identifier.startswith('collection.related') or \
|
if size == '0' or not hub_identifier.startswith('collection.related') or \
|
||||||
media_type not in children_results_list.keys():
|
media_type not in list(children_results_list.keys()):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result_data = []
|
result_data = []
|
||||||
|
@ -2280,7 +2289,7 @@ class PmsConnect(object):
|
||||||
}
|
}
|
||||||
children_results_list[media_type].append(children_output)
|
children_results_list[media_type].append(children_output)
|
||||||
|
|
||||||
output = {'results_count': sum(len(s) for s in children_results_list.items()),
|
output = {'results_count': sum(len(s) for s in list(children_results_list.items())),
|
||||||
'results_list': children_results_list,
|
'results_list': children_results_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2648,9 +2657,9 @@ class PmsConnect(object):
|
||||||
img = '{}/{}'.format(img.rstrip('/'), int(time.time()))
|
img = '{}/{}'.format(img.rstrip('/'), int(time.time()))
|
||||||
|
|
||||||
if clip:
|
if clip:
|
||||||
params = {'url': '%s&%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
|
params = {'url': '%s&%s' % (img, urllib.parse.urlencode({'X-Plex-Token': self.token}))}
|
||||||
else:
|
else:
|
||||||
params = {'url': 'http://127.0.0.1:32400%s?%s' % (img, urllib.urlencode({'X-Plex-Token': self.token}))}
|
params = {'url': 'http://127.0.0.1:32400%s?%s' % (img, urllib.parse.urlencode({'X-Plex-Token': self.token}))}
|
||||||
|
|
||||||
params['width'] = width
|
params['width'] = width
|
||||||
params['height'] = height
|
params['height'] = height
|
||||||
|
@ -2663,7 +2672,7 @@ class PmsConnect(object):
|
||||||
if blur:
|
if blur:
|
||||||
params['blur'] = blur
|
params['blur'] = blur
|
||||||
|
|
||||||
uri = '/photo/:/transcode?%s' % urllib.urlencode(params)
|
uri = '/photo/:/transcode?%s' % urllib.parse.urlencode(params)
|
||||||
result = self.request_handler.make_request(uri=uri,
|
result = self.request_handler.make_request(uri=uri,
|
||||||
request_type='GET',
|
request_type='GET',
|
||||||
return_type=True)
|
return_type=True)
|
||||||
|
@ -2705,7 +2714,7 @@ class PmsConnect(object):
|
||||||
|
|
||||||
for h in hubs:
|
for h in hubs:
|
||||||
if helpers.get_xml_attr(h, 'size') == '0' or \
|
if helpers.get_xml_attr(h, 'size') == '0' or \
|
||||||
helpers.get_xml_attr(h, 'type') not in search_results_list.keys():
|
helpers.get_xml_attr(h, 'type') not in list(search_results_list.keys()):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if h.getElementsByTagName('Video'):
|
if h.getElementsByTagName('Video'):
|
||||||
|
@ -2737,7 +2746,7 @@ class PmsConnect(object):
|
||||||
metadata = self.get_metadata_details(rating_key=rating_key)
|
metadata = self.get_metadata_details(rating_key=rating_key)
|
||||||
search_results_list[metadata['media_type']].append(metadata)
|
search_results_list[metadata['media_type']].append(metadata)
|
||||||
|
|
||||||
output = {'results_count': sum(len(s) for s in search_results_list.values()),
|
output = {'results_count': sum(len(s) for s in list(search_results_list.values())),
|
||||||
'results_list': search_results_list
|
'results_list': search_results_list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -13,6 +15,9 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
@ -21,13 +26,13 @@ import collections
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import plexpy.lock
|
from plexpy import lock
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
# Dictionary with last request times, for rate limiting.
|
# Dictionary with last request times, for rate limiting.
|
||||||
last_requests = collections.defaultdict(int)
|
last_requests = collections.defaultdict(int)
|
||||||
fake_lock = plexpy.lock.FakeLock()
|
fake_lock = lock.FakeLock()
|
||||||
|
|
||||||
|
|
||||||
def request_response(url, method="get", auto_raise=True,
|
def request_response(url, method="get", auto_raise=True,
|
||||||
|
@ -319,7 +324,7 @@ def server_message(response, return_msg=False):
|
||||||
|
|
||||||
if return_msg:
|
if return_msg:
|
||||||
try:
|
try:
|
||||||
return unicode(message, 'UTF-8')
|
return str(message, 'UTF-8')
|
||||||
except:
|
except:
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,10 +15,13 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
import common
|
from plexpy import common
|
||||||
import users
|
from plexpy import users
|
||||||
|
|
||||||
|
|
||||||
def get_session_info():
|
def get_session_info():
|
||||||
|
@ -216,14 +221,14 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
||||||
|
|
||||||
for d in list_of_dicts:
|
for d in list_of_dicts:
|
||||||
if session_user_id and not (str(d.get('user_id')) == session_user_id or d.get('user') == session_user):
|
if session_user_id and not (str(d.get('user_id')) == session_user_id or d.get('user') == session_user):
|
||||||
for k, v in keys_to_mask.iteritems():
|
for k, v in keys_to_mask.items():
|
||||||
if k in d: d[k] = keys_to_mask[k]
|
if k in d: d[k] = keys_to_mask[k]
|
||||||
|
|
||||||
if not mask_metadata:
|
if not mask_metadata:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if str(d.get('section_id','')) not in session_library_ids:
|
if str(d.get('section_id','')) not in session_library_ids:
|
||||||
for k, v in metadata_to_mask.iteritems():
|
for k, v in metadata_to_mask.items():
|
||||||
if k in d: d[k] = metadata_to_mask[k]
|
if k in d: d[k] = metadata_to_mask[k]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -247,7 +252,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
||||||
if d_content_rating in f_content_rating or set(d_labels).intersection(set(f_labels)):
|
if d_content_rating in f_content_rating or set(d_labels).intersection(set(f_labels)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for k, v in metadata_to_mask.iteritems():
|
for k, v in metadata_to_mask.items():
|
||||||
if k in d: d[k] = metadata_to_mask[k]
|
if k in d: d[k] = metadata_to_mask[k]
|
||||||
|
|
||||||
return list_of_dicts
|
return list_of_dicts
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,18 +15,25 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import httpagentparser
|
import httpagentparser
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import database
|
from plexpy import database
|
||||||
import datatables
|
from plexpy import datatables
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import logger
|
from plexpy import logger
|
||||||
import plextv
|
from plexpy import plextv
|
||||||
import session
|
from plexpy import session
|
||||||
|
|
||||||
|
|
||||||
def refresh_users():
|
def refresh_users():
|
||||||
|
@ -509,7 +518,7 @@ class Users(object):
|
||||||
for item in result:
|
for item in result:
|
||||||
# Rename Mystery platform names
|
# Rename Mystery platform names
|
||||||
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
||||||
platform_name = next((v for k, v in common.PLATFORM_NAMES.iteritems() 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 = {'player_name': item['player'],
|
row = {'player_name': item['player'],
|
||||||
'platform': platform,
|
'platform': platform,
|
||||||
|
@ -717,7 +726,7 @@ class Users(object):
|
||||||
|
|
||||||
def get_user_names(self, kwargs=None):
|
def get_user_names(self, kwargs=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
user_cond = ''
|
user_cond = ''
|
||||||
if session.get_session_user_id():
|
if session.get_session_user_id():
|
||||||
user_cond = 'AND user_id = %s ' % session.get_session_user_id()
|
user_cond = 'AND user_id = %s ' % session.get_session_user_id()
|
||||||
|
@ -733,9 +742,9 @@ class Users(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("Tautulli Users :: Unable to execute database query for get_user_names: %s." % e)
|
logger.warn("Tautulli Users :: Unable to execute database query for get_user_names: %s." % e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return session.friendly_name_to_username(result)
|
return session.friendly_name_to_username(result)
|
||||||
|
|
||||||
def get_tokens(self, user_id=None):
|
def get_tokens(self, user_id=None):
|
||||||
if user_id:
|
if user_id:
|
||||||
try:
|
try:
|
||||||
|
@ -757,7 +766,7 @@ class Users(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_filters(self, user_id=None):
|
def get_filters(self, user_id=None):
|
||||||
import urlparse
|
import urllib.parse
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return {}
|
return {}
|
||||||
|
@ -772,13 +781,13 @@ class Users(object):
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
filters_list = {}
|
filters_list = {}
|
||||||
for k, v in result.iteritems():
|
for k, v in result.items():
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
for f in v.split('|'):
|
for f in v.split('|'):
|
||||||
if 'contentRating=' in f or 'label=' in f:
|
if 'contentRating=' in f or 'label=' in f:
|
||||||
filters.update(dict(urlparse.parse_qsl(f)))
|
filters.update(dict(urllib.parse.parse_qsl(f)))
|
||||||
|
|
||||||
filters['content_rating'] = tuple(f for f in filters.pop('contentRating', '').split(',') if f)
|
filters['content_rating'] = tuple(f for f in filters.pop('contentRating', '').split(',') if f)
|
||||||
filters['labels'] = tuple(f for f in filters.pop('label', '').split(',') if f)
|
filters['labels'] = tuple(f for f in filters.pop('label', '').split(',') if f)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -13,6 +15,12 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from past.utils import old_div
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
@ -20,9 +28,9 @@ import subprocess
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import common
|
from plexpy import common
|
||||||
import logger
|
from plexpy import logger
|
||||||
import request
|
from plexpy import request
|
||||||
|
|
||||||
|
|
||||||
def runGit(args):
|
def runGit(args):
|
||||||
|
@ -44,7 +52,7 @@ def runGit(args):
|
||||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + plexpy.PROG_DIR)
|
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + plexpy.PROG_DIR)
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=plexpy.PROG_DIR)
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=plexpy.PROG_DIR)
|
||||||
output, err = p.communicate()
|
output, err = p.communicate()
|
||||||
output = output.strip()
|
output = output.strip().decode()
|
||||||
|
|
||||||
logger.debug('Git output: ' + output)
|
logger.debug('Git output: ' + output)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -372,7 +380,7 @@ def read_changelog(latest_only=False, since_prev_release=False):
|
||||||
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
||||||
|
|
||||||
elif line_list_match:
|
elif line_list_match:
|
||||||
line_level = len(line_list_match.group(1)) / 2
|
line_level = old_div(len(line_list_match.group(1)), 2)
|
||||||
line_text = line_list_match.group(2)
|
line_text = line_list_match.group(2)
|
||||||
|
|
||||||
if line_level > prev_level:
|
if line_level > prev_level:
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,6 +17,11 @@
|
||||||
|
|
||||||
# Mostly borrowed from https://github.com/trakt/Plex-Trakt-Scrobbler
|
# Mostly borrowed from https://github.com/trakt/Plex-Trakt-Scrobbler
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
@ -22,11 +29,12 @@ import time
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_handler
|
from plexpy import activity_handler
|
||||||
import activity_pinger
|
from plexpy import activity_pinger
|
||||||
import activity_processor
|
from plexpy import activity_processor
|
||||||
import database
|
from plexpy import database
|
||||||
import logger
|
from plexpy import logger
|
||||||
|
|
||||||
|
|
||||||
name = 'websocket'
|
name = 'websocket'
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -18,15 +20,21 @@
|
||||||
# Form based authentication for CherryPy. Requires the
|
# Form based authentication for CherryPy. Requires the
|
||||||
# Session tool to be loaded.
|
# Session tool to be loaded.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from urllib import quote, unquote
|
from urllib.parse import quote, unquote
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from hashing_passwords import check_hash
|
from hashing_passwords import check_hash
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import logger
|
from plexpy import logger
|
||||||
from plexpy.database import MonitorDatabase
|
from plexpy.database import MonitorDatabase
|
||||||
from plexpy.users import Users, refresh_users
|
from plexpy.users import Users, refresh_users
|
||||||
from plexpy.plextv import PlexTV
|
from plexpy.plextv import PlexTV
|
||||||
|
@ -258,15 +266,15 @@ class AuthController(object):
|
||||||
use_oauth = 'Plex OAuth' if oauth else 'form'
|
use_oauth = 'Plex OAuth' if oauth else 'form'
|
||||||
logger.debug("Tautulli WebAuth :: %s user '%s' logged into Tautulli using %s login."
|
logger.debug("Tautulli WebAuth :: %s user '%s' logged into Tautulli using %s login."
|
||||||
% (user_group.capitalize(), username, use_oauth))
|
% (user_group.capitalize(), username, use_oauth))
|
||||||
|
|
||||||
def on_logout(self, username, user_group):
|
def on_logout(self, username, user_group):
|
||||||
"""Called on logout"""
|
"""Called on logout"""
|
||||||
logger.debug("Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
|
logger.debug("Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
|
||||||
|
|
||||||
def get_loginform(self, redirect_uri=''):
|
def get_loginform(self, redirect_uri=''):
|
||||||
from plexpy.webserve import serve_template
|
from plexpy.webserve import serve_template
|
||||||
return serve_template(templatename="login.html", title="Login", redirect_uri=unquote(redirect_uri))
|
return serve_template(templatename="login.html", title="Login", redirect_uri=unquote(redirect_uri))
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def index(self, *args, **kwargs):
|
def index(self, *args, **kwargs):
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# This file is part of Tautulli.
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,11 +15,19 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import next
|
||||||
|
from builtins import str
|
||||||
|
from past.builtins import basestring
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib.static import serve_file, serve_download
|
from cherrypy.lib.static import serve_file, serve_download
|
||||||
|
@ -30,30 +40,30 @@ from mako import exceptions
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import activity_pinger
|
from plexpy import activity_pinger
|
||||||
import common
|
from plexpy import common
|
||||||
import config
|
from plexpy import config
|
||||||
import database
|
from plexpy import database
|
||||||
import datafactory
|
from plexpy import datafactory
|
||||||
import graphs
|
from plexpy import graphs
|
||||||
import helpers
|
from plexpy import helpers
|
||||||
import http_handler
|
from plexpy import http_handler
|
||||||
import libraries
|
from plexpy import libraries
|
||||||
import log_reader
|
from plexpy import log_reader
|
||||||
import logger
|
from plexpy import logger
|
||||||
import newsletter_handler
|
from plexpy import newsletter_handler
|
||||||
import newsletters
|
from plexpy import newsletters
|
||||||
import mobile_app
|
from plexpy import mobile_app
|
||||||
import notification_handler
|
from plexpy import notification_handler
|
||||||
import notifiers
|
from plexpy import notifiers
|
||||||
import plextv
|
from plexpy import plextv
|
||||||
import plexivity_import
|
from plexpy import plexivity_import
|
||||||
import plexwatch_import
|
from plexpy import plexwatch_import
|
||||||
import pmsconnect
|
from plexpy import pmsconnect
|
||||||
import users
|
from plexpy import users
|
||||||
import versioncheck
|
from plexpy import versioncheck
|
||||||
import web_socket
|
from plexpy import web_socket
|
||||||
import webstart
|
from plexpy import webstart
|
||||||
from plexpy.api2 import API2
|
from plexpy.api2 import API2
|
||||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
|
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
|
||||||
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
|
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
|
||||||
|
@ -288,7 +298,7 @@ class WebInterface(object):
|
||||||
if '{machine_id}' in endpoint:
|
if '{machine_id}' in endpoint:
|
||||||
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
|
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
|
||||||
|
|
||||||
return base_url + endpoint + '?' + urllib.urlencode(kwargs)
|
return base_url + endpoint + '?' + urllib.parse.urlencode(kwargs)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
|
@ -685,7 +695,7 @@ class WebInterface(object):
|
||||||
# Alias 'title' to 'sort_title'
|
# Alias 'title' to 'sort_title'
|
||||||
if kwargs.get('order_column') == 'title':
|
if kwargs.get('order_column') == 'title':
|
||||||
kwargs['order_column'] = 'sort_title'
|
kwargs['order_column'] = 'sort_title'
|
||||||
|
|
||||||
# TODO: Find some one way to automatically get the columns
|
# TODO: Find some one way to automatically get the columns
|
||||||
dt_columns = [("added_at", True, False),
|
dt_columns = [("added_at", True, False),
|
||||||
("sort_title", True, True),
|
("sort_title", True, True),
|
||||||
|
@ -2364,13 +2374,13 @@ class WebInterface(object):
|
||||||
try:
|
try:
|
||||||
temp_loglevel_and_time = l.split(' - ', 1)
|
temp_loglevel_and_time = l.split(' - ', 1)
|
||||||
loglvl = temp_loglevel_and_time[1].split(' ::', 1)[0].strip()
|
loglvl = temp_loglevel_and_time[1].split(' ::', 1)[0].strip()
|
||||||
msg = helpers.sanitize(unicode(l.split(' : ', 1)[1].replace('\n', ''), 'utf-8'))
|
msg = helpers.sanitize(str(l.split(' : ', 1)[1].replace('\n', ''), 'utf-8'))
|
||||||
fa([temp_loglevel_and_time[0], loglvl, msg])
|
fa([temp_loglevel_and_time[0], loglvl, msg])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# Add traceback message to previous msg.
|
# Add traceback message to previous msg.
|
||||||
tl = (len(filt) - 1)
|
tl = (len(filt) - 1)
|
||||||
n = len(l) - len(l.lstrip(' '))
|
n = len(l) - len(l.lstrip(' '))
|
||||||
ll = ' ' * (2 * n) + helpers.sanitize(unicode(l[n:], 'utf-8'))
|
ll = ' ' * (2 * n) + helpers.sanitize(str(l[n:], 'utf-8'))
|
||||||
filt[tl][2] += '<br>' + ll
|
filt[tl][2] += '<br>' + ll
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -2918,14 +2928,14 @@ class WebInterface(object):
|
||||||
|
|
||||||
# Remove config with 'hsec-' prefix and change home_sections to list
|
# Remove config with 'hsec-' prefix and change home_sections to list
|
||||||
if kwargs.get('home_sections'):
|
if kwargs.get('home_sections'):
|
||||||
for k in kwargs.keys():
|
for k in list(kwargs.keys()):
|
||||||
if k.startswith('hsec-'):
|
if k.startswith('hsec-'):
|
||||||
del kwargs[k]
|
del kwargs[k]
|
||||||
kwargs['home_sections'] = kwargs['home_sections'].split(',')
|
kwargs['home_sections'] = kwargs['home_sections'].split(',')
|
||||||
|
|
||||||
# Remove config with 'hscard-' prefix and change home_stats_cards to list
|
# Remove config with 'hscard-' prefix and change home_stats_cards to list
|
||||||
if kwargs.get('home_stats_cards'):
|
if kwargs.get('home_stats_cards'):
|
||||||
for k in kwargs.keys():
|
for k in list(kwargs.keys()):
|
||||||
if k.startswith('hscard-'):
|
if k.startswith('hscard-'):
|
||||||
del kwargs[k]
|
del kwargs[k]
|
||||||
kwargs['home_stats_cards'] = kwargs['home_stats_cards'].split(',')
|
kwargs['home_stats_cards'] = kwargs['home_stats_cards'].split(',')
|
||||||
|
@ -2935,7 +2945,7 @@ class WebInterface(object):
|
||||||
|
|
||||||
# Remove config with 'hlcard-' prefix and change home_library_cards to list
|
# Remove config with 'hlcard-' prefix and change home_library_cards to list
|
||||||
if kwargs.get('home_library_cards'):
|
if kwargs.get('home_library_cards'):
|
||||||
for k in kwargs.keys():
|
for k in list(kwargs.keys()):
|
||||||
if k.startswith('hlcard-'):
|
if k.startswith('hlcard-'):
|
||||||
del kwargs[k]
|
del kwargs[k]
|
||||||
kwargs['home_library_cards'] = kwargs['home_library_cards'].split(',')
|
kwargs['home_library_cards'] = kwargs['home_library_cards'].split(',')
|
||||||
|
@ -3304,7 +3314,7 @@ class WebInterface(object):
|
||||||
'type': param['type'],
|
'type': param['type'],
|
||||||
'value': param['value']
|
'value': param['value']
|
||||||
}
|
}
|
||||||
for category in common.NOTIFICATION_PARAMETERS
|
for category in common.NOTIFICATION_PARAMETERS
|
||||||
for param in category['parameters']]
|
for param in category['parameters']]
|
||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
|
@ -3864,7 +3874,7 @@ class WebInterface(object):
|
||||||
if git_branch == plexpy.CONFIG.GIT_BRANCH:
|
if git_branch == plexpy.CONFIG.GIT_BRANCH:
|
||||||
logger.error("Already on the %s branch" % git_branch)
|
logger.error("Already on the %s branch" % git_branch)
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
||||||
|
|
||||||
# Set the new git remote and branch
|
# Set the new git remote and branch
|
||||||
plexpy.CONFIG.__setattr__('GIT_REMOTE', git_remote)
|
plexpy.CONFIG.__setattr__('GIT_REMOTE', git_remote)
|
||||||
plexpy.CONFIG.__setattr__('GIT_BRANCH', git_branch)
|
plexpy.CONFIG.__setattr__('GIT_BRANCH', git_branch)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
# This file is part of Tautulli.
|
||||||
#
|
#
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
# Tautulli is free software: you can redistribute it and/or modify
|
||||||
|
@ -13,14 +15,21 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from future import standard_library
|
||||||
|
standard_library.install_aliases()
|
||||||
|
from builtins import str
|
||||||
|
from builtins import object
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from urllib import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import cherrypy
|
from plexpy import logger
|
||||||
import logger
|
from plexpy import webauth
|
||||||
import webauth
|
|
||||||
from plexpy.helpers import create_https_certificates
|
from plexpy.helpers import create_https_certificates
|
||||||
from plexpy.webserve import WebInterface
|
from plexpy.webserve import WebInterface
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue