Run futurize --stage1

This commit is contained in:
JonnyWong16 2019-11-23 19:11:42 -08:00
parent 221be380ee
commit ab6196589b
36 changed files with 736 additions and 497 deletions

View file

@ -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

View file

@ -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
} }

View file

@ -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

View file

@ -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'])

View file

@ -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}

View file

@ -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:

View file

@ -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):
""" """

View file

@ -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',

View file

@ -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)

View file

@ -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)

View file

@ -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]

View file

@ -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']

View file

@ -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, ' \

View file

@ -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('<', '&lt;').replace('>', '&gt;') return str(obj).replace('<', '&lt;').replace('>', '&gt;')
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

View file

@ -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

View file

@ -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'):

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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]

View file

@ -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):

View file

@ -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:

View file

@ -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):

View file

@ -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
} }

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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")

View file

@ -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 = '&nbsp;' * (2 * n) + helpers.sanitize(unicode(l[n:], 'utf-8')) ll = '&nbsp;' * (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)

View file

@ -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