diff --git a/jellypy/__init__.py b/jellypy/__init__.py index e50710bb..2b575e3a 100644 --- a/jellypy/__init__.py +++ b/jellypy/__init__.py @@ -15,6 +15,7 @@ import datetime import os +import queue import sqlite3 import subprocess import sys diff --git a/jellypy/api2.py b/jellypy/api2.py index 9805b1f2..88978c03 100644 --- a/jellypy/api2.py +++ b/jellypy/api2.py @@ -27,7 +27,6 @@ from io import open import cherrypy import xmltodict -from hashing_passwords import check_hash import jellypy from jellypy import common @@ -43,6 +42,7 @@ from jellypy import newsletter_handler from jellypy import newsletters from jellypy import plextv from jellypy import users +from jellypy.password import check_hash class API2(object): diff --git a/jellypy/certgen.py b/jellypy/certgen.py new file mode 100644 index 00000000..2533ee49 --- /dev/null +++ b/jellypy/certgen.py @@ -0,0 +1,110 @@ +# -*- coding: latin-1 -*- +# +# Copyright (C) AB Strakt +# Copyright (C) Jean-Paul Calderone +# See LICENSE for details. + +""" +Certificate generation module. +""" + +from OpenSSL import crypto + +TYPE_RSA = crypto.TYPE_RSA +TYPE_DSA = crypto.TYPE_DSA + + +def createKeyPair(type, bits): + """ + Create a public/private key pair. + Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA + bits - Number of bits to use in the key + Returns: The public/private key pair in a PKey object + """ + pkey = crypto.PKey() + pkey.generate_key(type, bits) + return pkey + + +def createCertRequest(pkey, digest="sha256", **name): + """ + Create a certificate request. + Arguments: pkey - The key to associate with the request + digest - Digestion method to use for signing, default is sha256 + **name - The name of the subject of the request, possible + arguments are: + C - Country name + ST - State or province name + L - Locality name + O - Organization name + OU - Organizational unit name + CN - Common name + emailAddress - E-mail address + Returns: The certificate request in an X509Req object + """ + req = crypto.X509Req() + subj = req.get_subject() + + for key, value in name.items(): + setattr(subj, key, value) + + req.set_pubkey(pkey) + req.sign(pkey, digest) + return req + + +def createCertificate(req, issuerCertKey, serial, validityPeriod, digest="sha256"): + """ + Generate a certificate given a certificate request. + Arguments: req - Certificate request to use + issuerCert - The certificate of the issuer + issuerKey - The private key of the issuer + serial - Serial number for the certificate + notBefore - Timestamp (relative to now) when the certificate + starts being valid + notAfter - Timestamp (relative to now) when the certificate + stops being valid + digest - Digest method to use for signing, default is sha256 + Returns: The signed certificate in an X509 object + """ + issuerCert, issuerKey = issuerCertKey + notBefore, notAfter = validityPeriod + cert = crypto.X509() + cert.set_serial_number(serial) + cert.gmtime_adj_notBefore(notBefore) + cert.gmtime_adj_notAfter(notAfter) + cert.set_issuer(issuerCert.get_subject()) + cert.set_subject(req.get_subject()) + cert.set_pubkey(req.get_pubkey()) + cert.sign(issuerKey, digest) + return cert + + +def createSelfSignedCertificate(issuerName, issuerKey, serial, notBefore, notAfter, altNames, digest="sha256"): + """ + Generate a certificate given a certificate request. + Arguments: issuerName - The name of the issuer + issuerKey - The private key of the issuer + serial - Serial number for the certificate + notBefore - Timestamp (relative to now) when the certificate + starts being valid + notAfter - Timestamp (relative to now) when the certificate + stops being valid + altNames - The alternative names + digest - Digest method to use for signing, default is sha256 + Returns: The signed certificate in an X509 object + """ + cert = crypto.X509() + cert.set_version(2) + cert.set_serial_number(serial) + cert.get_subject().CN = issuerName + cert.gmtime_adj_notBefore(notBefore) + cert.gmtime_adj_notAfter(notAfter) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(issuerKey) + + if altNames: + cert.add_extensions([crypto.X509Extension(b"subjectAltName", False, altNames)]) + + cert.sign(issuerKey, digest) + return cert diff --git a/jellypy/classes.py b/jellypy/classes.py deleted file mode 100644 index 9d2a2819..00000000 --- a/jellypy/classes.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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 . - -######################################### -## Stolen from Sick-Beard's classes.py ## -######################################### - -from jellypy.common import USER_AGENT - - -class PlexPyURLopener(FancyURLopener): - version = USER_AGENT - - -class AuthURLOpener(PlexPyURLopener): - """ - URLOpener class that supports http auth without needing interactive password entry. - If the provided username/password don't work it simply fails. - - user: username to use for HTTP auth - pw: password to use for HTTP auth - """ - - def __init__(self, user, pw): - self.username = user - self.password = pw - - # remember if we've tried the username/password before - self.numTries = 0 - - # call the base class - FancyURLopener.__init__(self) - - def prompt_user_passwd(self, host, realm): - """ - Override this function and instead of prompting just give the - username/password that were provided when the class was instantiated. - """ - - # if this is the first try then provide a username/password - if self.numTries == 0: - self.numTries = 1 - return (self.username, self.password) - - # if we've tried before then return blank which cancels the request - else: - return ('', '') - - # this is pretty much just a hack for convenience - def openit(self, url): - self.numTries = 0 - return PlexPyURLopener.open(self, url) diff --git a/jellypy/helpers.py b/jellypy/helpers.py index b17d0614..a5eeca43 100644 --- a/jellypy/helpers.py +++ b/jellypy/helpers.py @@ -32,6 +32,8 @@ import time import unicodedata from collections import OrderedDict from functools import reduce, wraps +from itertools import zip_longest, islice +from urllib.parse import urlencode from xml.dom import minidom import arrow @@ -46,7 +48,6 @@ from cloudinary.uploader import upload from cloudinary.utils import cloudinary_url import jellypy - from jellypy import common from jellypy import logger from jellypy import request diff --git a/jellypy/notifiers.py b/jellypy/notifiers.py index 2338e51d..0bd87ce7 100644 --- a/jellypy/notifiers.py +++ b/jellypy/notifiers.py @@ -17,26 +17,29 @@ import base64 -import bleach -import json -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText import email.utils -from paho.mqtt.publish import single +import json import os import re -import requests import smtplib import subprocess import sys import threading import time +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from urllib.parse import urlencode, urlparse + +import bleach +import requests +from paho.mqtt.publish import single try: from Cryptodome.Protocol.KDF import PBKDF2 from Cryptodome.Cipher import AES from Cryptodome.Random import get_random_bytes from Cryptodome.Hash import HMAC, SHA1 + CRYPTODOME = True except ImportError: try: @@ -44,34 +47,23 @@ except ImportError: from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Hash import HMAC, SHA1 + CRYPTODOME = True except ImportError: CRYPTODOME = False import gntp.notifier -import facebook import twitter import jellypy -if jellypy.PYTHON2: - import common - import database - import helpers - import logger - import mobile_app - import pmsconnect - import request - import users -else: - from jellypy import common - from jellypy import database - from jellypy import helpers - from jellypy import logger - from jellypy import mobile_app - from jellypy import pmsconnect - from jellypy import request - from jellypy import users - +from jellypy import common +from jellypy import database +from jellypy import helpers +from jellypy import logger +from jellypy import mobile_app +from jellypy import pmsconnect +from jellypy import request +from jellypy import users BROWSER_NOTIFIERS = {} @@ -135,12 +127,6 @@ def available_notification_agents(): 'class': EMAIL, 'action_types': ('all',) }, - {'label': 'Facebook', - 'name': 'facebook', - 'id': AGENT_IDS['facebook'], - 'class': FACEBOOK, - 'action_types': ('all',) - }, {'label': 'GroupMe', 'name': 'groupme', 'id': AGENT_IDS['groupme'], @@ -788,7 +774,7 @@ class PrettyMetadata(object): @staticmethod def get_parameters(): parameters = {param['value']: param['name'] - for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']} + for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']} parameters[''] = '' return parameters @@ -852,7 +838,8 @@ class Notifier(object): if response is not None and 400 <= response.status_code < 500: verify_msg = " Verify your notification agent settings are correct." - logger.error("Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME)) + logger.error( + "Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME)) if err_msg: logger.error("Tautulli Notifiers :: {}".format(err_msg)) @@ -907,7 +894,7 @@ class ANDROIDAPP(Notifier): 'rating_key': pretty_metadata.parameters.get('rating_key', ''), 'poster_thumb': pretty_metadata.parameters.get('poster_thumb', '')} - #logger.debug("Plaintext data: {}".format(plaintext_data)) + # logger.debug("Plaintext data: {}".format(plaintext_data)) if CRYPTODOME: # Key generation @@ -918,7 +905,7 @@ class ANDROIDAPP(Notifier): key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations, prf=lambda p, s: HMAC.new(p, s, SHA1).digest()) - #logger.debug("Encryption key (base64): {}".format(base64.b64encode(key))) + # logger.debug("Encryption key (base64): {}".format(base64.b64encode(key))) # Encrypt using AES GCM nonce = get_random_bytes(16) @@ -926,10 +913,10 @@ class ANDROIDAPP(Notifier): encrypted_data, gcm_tag = cipher.encrypt_and_digest(json.dumps(plaintext_data).encode('utf-8')) encrypted_data += gcm_tag - #logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data))) - #logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag))) - #logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce))) - #logger.debug("Salt (base64): {}".format(base64.b64encode(salt))) + # logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data))) + # logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag))) + # logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce))) + # logger.debug("Salt (base64): {}".format(base64.b64encode(salt))) payload = {'app_id': mobile_app._ONESIGNAL_APP_ID, 'include_player_ids': [device['onesignal_id']], @@ -953,7 +940,7 @@ class ANDROIDAPP(Notifier): 'server_id': jellypy.CONFIG.PMS_UUID} } - #logger.debug("OneSignal payload: {}".format(payload)) + # logger.debug("OneSignal payload: {}".format(payload)) headers = {'Content-Type': 'application/json'} @@ -990,24 +977,25 @@ class ANDROIDAPP(Notifier): 'Please install the library to encrypt the notification contents. ' 'Instructions can be found in the ' 'FAQ.' , + 'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome' + % (jellypy.CONFIG.GIT_USER, jellypy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ.', 'input_type': 'help' - }) + }) else: config_option.append({ 'label': 'Note', 'description': 'The PyCryptodome library was found. ' 'The content of your notifications will be sent encrypted!', 'input_type': 'help' - }) + }) config_option[-1]['description'] += '

Notifications are sent using the ' \ - '' \ - 'OneSignal. Some user data is collected and cannot be encrypted. ' \ - 'Please read the ' \ - 'OneSignal Privacy Policy for more details.' + '' \ + 'OneSignal. Some user data is collected and cannot be encrypted. ' \ + 'Please read the ' \ + 'OneSignal Privacy Policy for more details.' devices = self.get_devices() @@ -1018,7 +1006,7 @@ class ANDROIDAPP(Notifier): '' 'Get the Android App and register a device.', 'input_type': 'help' - }) + }) else: config_option.append({ 'label': 'Device', @@ -1029,7 +1017,7 @@ class ANDROIDAPP(Notifier): 'register a new device with Tautulli.', 'input_type': 'select', 'select_options': devices - }) + }) config_option.append({ 'label': 'Priority', @@ -1038,7 +1026,7 @@ class ANDROIDAPP(Notifier): 'description': 'Set the notification priority.', 'input_type': 'select', 'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'} - }) + }) return config_option @@ -1081,7 +1069,7 @@ class BOXCAR(Notifier): 'flourish': 'Flourish', 'harp': 'Harp', 'light': 'Light', - 'magic-chime':'Magic Chime', + 'magic-chime': 'Magic Chime', 'magic-coin': 'Magic Coin', 'no-sound': 'No Sound', 'notifier-1': 'Notifier (1)', @@ -1507,191 +1495,6 @@ class EMAIL(Notifier): return config_option -class FACEBOOK(Notifier): - """ - Facebook notifications - """ - NAME = 'Facebook' - _DEFAULT_CONFIG = {'redirect_uri': '', - 'access_token': '', - 'app_id': '', - 'app_secret': '', - 'group_id': '', - 'incl_subject': 1, - 'incl_card': 0, - 'movie_provider': '', - 'tv_provider': '', - 'music_provider': '' - } - - def _get_authorization(self, app_id='', app_secret='', redirect_uri=''): - # Temporarily store settings in the config so we can retrieve them in Facebook step 2. - # Assume the user won't be requesting authorization for multiple Facebook notifiers at the same time. - jellypy.CONFIG.FACEBOOK_APP_ID = app_id - jellypy.CONFIG.FACEBOOK_APP_SECRET = app_secret - jellypy.CONFIG.FACEBOOK_REDIRECT_URI = redirect_uri - jellypy.CONFIG.FACEBOOK_TOKEN = 'temp' - - return facebook.auth_url(app_id=app_id, - canvas_url=redirect_uri, - perms=['publish_to_groups']) - - def _get_credentials(self, code=''): - logger.info("Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME)) - - app_id = jellypy.CONFIG.FACEBOOK_APP_ID - app_secret = jellypy.CONFIG.FACEBOOK_APP_SECRET - redirect_uri = jellypy.CONFIG.FACEBOOK_REDIRECT_URI - - try: - # Request user access token - api = facebook.GraphAPI(version='2.12') - response = api.get_access_token_from_code(code=code, - redirect_uri=redirect_uri, - app_id=app_id, - app_secret=app_secret) - access_token = response['access_token'] - - # Request extended user access token - api = facebook.GraphAPI(access_token=access_token, version='2.12') - response = api.extend_access_token(app_id=app_id, - app_secret=app_secret) - - jellypy.CONFIG.FACEBOOK_TOKEN = response['access_token'] - except Exception as e: - logger.error("Tautulli Notifiers :: Error requesting {name} access token: {e}".format(name=self.NAME, e=e)) - jellypy.CONFIG.FACEBOOK_TOKEN = '' - - # Clear out temporary config values - jellypy.CONFIG.FACEBOOK_APP_ID = '' - jellypy.CONFIG.FACEBOOK_APP_SECRET = '' - jellypy.CONFIG.FACEBOOK_REDIRECT_URI = '' - - return jellypy.CONFIG.FACEBOOK_TOKEN - - def _post_facebook(self, **data): - if self.config['group_id']: - api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.12') - - try: - api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data) - logger.info("Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME)) - return True - except Exception as e: - logger.error("Tautulli Notifiers :: Error sending {name} post: {e}".format(name=self.NAME, e=e)) - return False - - else: - logger.error("Tautulli Notifiers :: Error sending {name} post: No {name} Group ID provided.".format(name=self.NAME)) - return False - - def agent_notify(self, subject='', body='', action='', **kwargs): - if self.config['incl_subject']: - text = subject + '\r\n' + body - else: - text = body - - data = {'message': text} - - if self.config['incl_card'] and kwargs.get('parameters', {}).get('media_type'): - # Grab formatted metadata - pretty_metadata = PrettyMetadata(kwargs['parameters']) - - if pretty_metadata.media_type == 'movie': - provider = self.config['movie_provider'] - elif pretty_metadata.media_type in ('show', 'season', 'episode'): - provider = self.config['tv_provider'] - elif pretty_metadata.media_type in ('artist', 'album', 'track'): - provider = self.config['music_provider'] - else: - provider = None - - data['link'] = pretty_metadata.get_provider_link(provider) - - return self._post_facebook(**data) - - def _return_config_options(self): - config_option = [{'label': 'OAuth Redirect URI', - 'value': self.config['redirect_uri'], - 'name': 'facebook_redirect_uri', - 'description': 'Fill in this address for the "Valid OAuth redirect URIs" ' - 'in your Facebook App.', - 'input_type': 'text' - }, - {'label': 'Facebook App ID', - 'value': self.config['app_id'], - 'name': 'facebook_app_id', - 'description': 'Your Facebook app ID.', - 'input_type': 'text' - }, - {'label': 'Facebook App Secret', - 'value': self.config['app_secret'], - 'name': 'facebook_app_secret', - 'description': 'Your Facebook app secret.', - 'input_type': 'text' - }, - {'label': 'Request Authorization', - 'value': 'Request Authorization', - 'name': 'facebook_facebook_auth', - 'description': 'Request Facebook authorization. (Ensure you allow the browser pop-up).', - 'input_type': 'button' - }, - {'label': 'Facebook Access Token', - 'value': self.config['access_token'], - 'name': 'facebook_access_token', - 'description': 'Your Facebook access token. ' - 'Automatically filled in after requesting authorization.', - 'input_type': 'text' - }, - {'label': 'Facebook Group ID', - 'value': self.config['group_id'], - 'name': 'facebook_group_id', - 'description': 'Your Facebook Group ID.', - 'input_type': 'text' - }, - {'label': 'Include Subject Line', - 'value': self.config['incl_subject'], - 'name': 'facebook_incl_subject', - 'description': 'Include the subject line with the notifications.', - 'input_type': 'checkbox' - }, - {'label': 'Include Rich Metadata Info', - 'value': self.config['incl_card'], - 'name': 'facebook_incl_card', - 'description': 'Include an info card with a poster and metadata with the notifications.
' - 'Note: Image Hosting ' - 'must be enabled under the notifications settings tab.', - 'input_type': 'checkbox' - }, - {'label': 'Movie Link Source', - 'value': self.config['movie_provider'], - 'name': 'facebook_movie_provider', - 'description': 'Select the source for movie links on the info cards. Leave blank to disable.
' - 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', - 'input_type': 'select', - 'select_options': PrettyMetadata().get_movie_providers() - }, - {'label': 'TV Show Link Source', - 'value': self.config['tv_provider'], - 'name': 'facebook_tv_provider', - 'description': 'Select the source for tv show links on the info cards. Leave blank to disable.
' - 'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.', - 'input_type': 'select', - 'select_options': PrettyMetadata().get_tv_providers() - }, - {'label': 'Music Link Source', - 'value': self.config['music_provider'], - 'name': 'facebook_music_provider', - 'description': 'Select the source for music links on the info cards. Leave blank to disable.', - 'input_type': 'select', - 'select_options': PrettyMetadata().get_music_providers() - } - ] - - return config_option - - class GROUPME(Notifier): """ GroupMe notifications @@ -1715,7 +1518,7 @@ class GROUPME(Notifier): pretty_metadata = PrettyMetadata(kwargs.get('parameters')) # Retrieve the poster from Plex - result = pmsconnect.PmsConnect().get_image(img=pretty_metadata.parameters.get('poster_thumb','')) + result = pmsconnect.PmsConnect().get_image(img=pretty_metadata.parameters.get('poster_thumb', '')) if result and result[0]: poster_content = result[0] else: @@ -1811,12 +1614,13 @@ class GROWL(Notifier): logger.error("Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME)) return False except gntp.notifier.errors.AuthError: - logger.error("Tautulli Notifiers :: {name} notification failed: authentication error".format(name=self.NAME)) + logger.error( + "Tautulli Notifiers :: {name} notification failed: authentication error".format(name=self.NAME)) return False # Send it, including an image image_file = os.path.join(str(jellypy.PROG_DIR), - "data/interfaces/default/images/logo-circle.png") + "data/interfaces/default/images/logo-circle.png") with open(image_file, 'rb') as f: image = f.read() @@ -1882,7 +1686,8 @@ class IFTTT(Notifier): 'value': self.config['key'], 'name': 'ifttt_key', 'description': 'Your IFTTT webhook key. You can get a key from' - ' here.', + ' here.', 'input_type': 'text' }, {'label': 'IFTTT Event', @@ -1961,10 +1766,13 @@ class JOIN(Notifier): return True else: error_msg = response_data.get('errorMessage') - logger.error("Tautulli Notifiers :: {name} notification failed: {msg}".format(name=self.NAME, msg=error_msg)) + logger.error( + "Tautulli Notifiers :: {name} notification failed: {msg}".format(name=self.NAME, msg=error_msg)) return False else: - logger.error("Tautulli Notifiers :: {name} notification failed: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r)) + logger.error( + "Tautulli Notifiers :: {name} notification failed: [{r.status_code}] {r.reason}".format(name=self.NAME, + r=r)) logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True))) return False @@ -1985,14 +1793,19 @@ class JOIN(Notifier): devices.update({d['deviceName']: d['deviceName'] for d in response_devices}) else: error_msg = response_data.get('errorMessage') - logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=error_msg)) + logger.error( + "Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, + msg=error_msg)) else: - logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r)) + logger.error( + "Tautulli Notifiers :: Unable to retrieve {name} devices list: [{r.status_code}] {r.reason}".format( + name=self.NAME, r=r)) logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True))) except Exception as e: - logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e)) + logger.error( + "Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e)) return devices @@ -2092,7 +1905,8 @@ class MQTT(Notifier): if self.config['password']: auth['password'] = self.config['password'] - single(self.config['topic'], payload=json.dumps(data), qos=self.config['qos'], retain=bool(self.config['retain']), + single(self.config['topic'], payload=json.dumps(data), qos=self.config['qos'], + retain=bool(self.config['retain']), hostname=self.config['broker'], port=self.config['port'], client_id=self.config['clientid'], keepalive=self.config['keep_alive'], auth=auth or None, protocol=self.config['protocol']) @@ -2202,6 +2016,7 @@ class OSX(Notifier): def wrapper(self, *args, **kwargs): return func(self, old_IMP, *args, **kwargs) + new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector, signature=old_IMP.signature) self.objc.classAddMethod(cls, SEL, new_IMP) @@ -2217,8 +2032,8 @@ class OSX(Notifier): try: self._swizzle(self.objc.lookUpClass('NSBundle'), - b'bundleIdentifier', - self._swizzled_bundleIdentifier) + b'bundleIdentifier', + self._swizzled_bundleIdentifier) NSUserNotification = self.objc.lookUpClass('NSUserNotification') NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') @@ -2323,9 +2138,11 @@ class PLEX(Notifier): image = os.path.join(jellypy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png")) for host in hosts: - logger.info("Tautulli Notifiers :: Sending notification command to {name} @ {host}".format(name=self.NAME, host=host)) + logger.info("Tautulli Notifiers :: Sending notification command to {name} @ {host}".format(name=self.NAME, + host=host)) try: - version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major'] + version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version'][ + 'major'] if version < 12: # Eden notification = subject + "," + body + "," + str(display_time) @@ -2410,7 +2227,7 @@ class PLEXMOBILEAPP(Notifier): if action == 'test': tests = [] for configuration in self.configurations: - tests.append(self.agent_notify(subject=subject, body=body, action='test_'+configuration)) + tests.append(self.agent_notify(subject=subject, body=body, action='test_' + configuration)) return all(tests) configuration_action = action.split('test_')[-1] @@ -2689,7 +2506,8 @@ class PUSHBULLET(Notifier): logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True))) except Exception as e: - logger.error("Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e)) + logger.error( + "Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e)) return devices @@ -3016,7 +2834,7 @@ class SCRIPTS(Notifier): 'TAUTULLI_APIKEY': jellypy.CONFIG.API_KEY, 'TAUTULLI_ENCODING': jellypy.SYS_ENCODING, 'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION - } + } if user_id: user_tokens = users.Users().get_tokens(user_id=user_id) @@ -3144,7 +2962,7 @@ class SCRIPTS(Notifier): def _return_config_options(self): config_option = [{'label': 'Supported File Types', 'description': '' + \ - ', '.join(self.script_exts) + '', + ', '.join(self.script_exts) + '', 'input_type': 'help' }, {'label': 'Script Folder', @@ -3518,7 +3336,7 @@ class TWITTER(Notifier): poster_url = '' if self.config['incl_poster'] and kwargs.get('parameters'): parameters = kwargs['parameters'] - poster_url = parameters.get('poster_url','') + poster_url = parameters.get('poster_url', '') # Hack to add media type to attachment if poster_url and not helpers.get_img_service(): @@ -3688,7 +3506,8 @@ class XBMC(Notifier): for host in hosts: logger.info("Tautulli Notifiers :: Sending notification command to XMBC @ " + host) try: - version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major'] + version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version'][ + 'major'] if version < 12: # Eden notification = subject + "," + body + "," + str(display_time) diff --git a/jellypy/password.py b/jellypy/password.py new file mode 100644 index 00000000..e1c29f70 --- /dev/null +++ b/jellypy/password.py @@ -0,0 +1,22 @@ +import binascii +import hashlib +import os + + +def make_hash(password): + salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii') + pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), + salt, 100000) + pwdhash = binascii.hexlify(pwdhash) + return (salt + pwdhash).decode('ascii') + + +def check_hash(password, stored_pw): + salt = stored_pw[:64] + stored_password = stored_pw[64:] + pwdhash = hashlib.pbkdf2_hmac('sha512', + password.encode('utf-8'), + salt.encode('ascii'), + 100000) + pwdhash = binascii.hexlify(pwdhash).decode('ascii') + return pwdhash == stored_password diff --git a/jellypy/plex.py b/jellypy/plex.py index e5fac724..2b520e67 100644 --- a/jellypy/plex.py +++ b/jellypy/plex.py @@ -15,16 +15,8 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -from __future__ import unicode_literals - from plexapi.server import PlexServer -import jellypy -if jellypy.PYTHON2: - import logger -else: - from jellypy import logger - class Plex(object): def __init__(self, url, token): diff --git a/jellypy/plexivity_import.py b/jellypy/plexivity_import.py deleted file mode 100644 index de561d5d..00000000 --- a/jellypy/plexivity_import.py +++ /dev/null @@ -1,456 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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 . - -import sqlite3 -from xml.dom import minidom - -import arrow - -from jellypy import activity_processor -from jellypy import database -from jellypy import helpers -from jellypy import logger -from jellypy import users - - -def extract_plexivity_xml(xml=None): - output = {} - clean_xml = helpers.latinToAscii(xml) - try: - xml_parse = minidom.parseString(clean_xml) - except: - logger.warn("Tautulli Importer :: Error parsing XML for Plexivity database.") - return None - - # I think Plexivity only tracked videos and not music? - xml_head = xml_parse.getElementsByTagName('Video') - if not xml_head: - logger.warn("Tautulli Importer :: Error parsing XML for Plexivity database.") - return None - - for a in xml_head: - rating_key = helpers.get_xml_attr(a, 'ratingKey') - added_at = helpers.get_xml_attr(a, 'addedAt') - art = helpers.get_xml_attr(a, 'art') - duration = helpers.get_xml_attr(a, 'duration') - grandparent_rating_key = helpers.get_xml_attr(a, 'grandparentRatingKey') - grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb') - grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle') - original_title = helpers.get_xml_attr(a, 'originalTitle') - guid = helpers.get_xml_attr(a, 'guid') - section_id = helpers.get_xml_attr(a, 'librarySectionID') - media_index = helpers.get_xml_attr(a, 'index') - originally_available_at = helpers.get_xml_attr(a, 'originallyAvailableAt') - last_viewed_at = helpers.get_xml_attr(a, 'lastViewedAt') - parent_rating_key = helpers.get_xml_attr(a, 'parentRatingKey') - parent_media_index = helpers.get_xml_attr(a, 'parentIndex') - parent_thumb = helpers.get_xml_attr(a, 'parentThumb') - parent_title = helpers.get_xml_attr(a, 'parentTitle') - rating = helpers.get_xml_attr(a, 'rating') - thumb = helpers.get_xml_attr(a, 'thumb') - media_type = helpers.get_xml_attr(a, 'type') - updated_at = helpers.get_xml_attr(a, 'updatedAt') - view_offset = helpers.get_xml_attr(a, 'viewOffset') - year = helpers.get_xml_attr(a, 'year') - studio = helpers.get_xml_attr(a, 'studio') - title = helpers.get_xml_attr(a, 'title') - tagline = helpers.get_xml_attr(a, 'tagline') - - directors = [] - if a.getElementsByTagName('Director'): - director_elem = a.getElementsByTagName('Director') - for b in director_elem: - directors.append(helpers.get_xml_attr(b, 'tag')) - - aspect_ratio = '' - audio_channels = None - audio_codec = '' - bitrate = None - container = '' - height = None - video_codec = '' - video_framerate = '' - video_resolution = '' - width = None - - if a.getElementsByTagName('Media'): - media_elem = a.getElementsByTagName('Media') - for c in media_elem: - aspect_ratio = helpers.get_xml_attr(c, 'aspectRatio') - audio_channels = helpers.get_xml_attr(c, 'audioChannels') - audio_codec = helpers.get_xml_attr(c, 'audioCodec') - bitrate = helpers.get_xml_attr(c, 'bitrate') - container = helpers.get_xml_attr(c, 'container') - height = helpers.get_xml_attr(c, 'height') - video_codec = helpers.get_xml_attr(c, 'videoCodec') - video_framerate = helpers.get_xml_attr(c, 'videoFrameRate') - video_resolution = helpers.get_xml_attr(c, 'videoResolution') - width = helpers.get_xml_attr(c, 'width') - - ip_address = '' - machine_id = '' - platform = '' - player = '' - - if a.getElementsByTagName('Player'): - player_elem = a.getElementsByTagName('Player') - for d in player_elem: - ip_address = helpers.get_xml_attr(d, 'address').split('::ffff:')[-1] - machine_id = helpers.get_xml_attr(d, 'machineIdentifier') - platform = helpers.get_xml_attr(d, 'platform') - player = helpers.get_xml_attr(d, 'title') - - transcode_audio_channels = None - transcode_audio_codec = '' - audio_decision = 'direct play' - transcode_container = '' - transcode_height = None - transcode_protocol = '' - transcode_video_codec = '' - video_decision = 'direct play' - transcode_width = None - - if a.getElementsByTagName('TranscodeSession'): - transcode_elem = a.getElementsByTagName('TranscodeSession') - for e in transcode_elem: - transcode_audio_channels = helpers.get_xml_attr(e, 'audioChannels') - transcode_audio_codec = helpers.get_xml_attr(e, 'audioCodec') - audio_decision = helpers.get_xml_attr(e, 'audioDecision') - transcode_container = helpers.get_xml_attr(e, 'container') - transcode_height = helpers.get_xml_attr(e, 'height') - transcode_protocol = helpers.get_xml_attr(e, 'protocol') - transcode_video_codec = helpers.get_xml_attr(e, 'videoCodec') - video_decision = helpers.get_xml_attr(e, 'videoDecision') - transcode_width = helpers.get_xml_attr(e, 'width') - - # Generate a combined transcode decision value - if video_decision == 'transcode' or audio_decision == 'transcode': - transcode_decision = 'transcode' - elif video_decision == 'copy' or audio_decision == 'copy': - transcode_decision = 'copy' - else: - transcode_decision = 'direct play' - - user_id = None - - if a.getElementsByTagName('User'): - user_elem = a.getElementsByTagName('User') - for f in user_elem: - user_id = helpers.get_xml_attr(f, 'id') - - writers = [] - if a.getElementsByTagName('Writer'): - writer_elem = a.getElementsByTagName('Writer') - for g in writer_elem: - writers.append(helpers.get_xml_attr(g, 'tag')) - - actors = [] - if a.getElementsByTagName('Role'): - actor_elem = a.getElementsByTagName('Role') - for h in actor_elem: - actors.append(helpers.get_xml_attr(h, 'tag')) - - genres = [] - if a.getElementsByTagName('Genre'): - genre_elem = a.getElementsByTagName('Genre') - for i in genre_elem: - genres.append(helpers.get_xml_attr(i, 'tag')) - - labels = [] - if a.getElementsByTagName('Lables'): - label_elem = a.getElementsByTagName('Lables') - for i in label_elem: - labels.append(helpers.get_xml_attr(i, 'tag')) - - output = {'rating_key': rating_key, - 'added_at': added_at, - 'art': art, - 'duration': duration, - 'grandparent_rating_key': grandparent_rating_key, - 'grandparent_thumb': grandparent_thumb, - 'title': title, - 'parent_title': parent_title, - 'grandparent_title': grandparent_title, - 'original_title': original_title, - 'tagline': tagline, - 'guid': guid, - 'section_id': section_id, - 'media_index': media_index, - 'originally_available_at': originally_available_at, - 'last_viewed_at': last_viewed_at, - 'parent_rating_key': parent_rating_key, - 'parent_media_index': parent_media_index, - 'parent_thumb': parent_thumb, - 'rating': rating, - 'thumb': thumb, - 'media_type': media_type, - 'updated_at': updated_at, - 'view_offset': view_offset, - 'year': year, - 'directors': directors, - 'aspect_ratio': aspect_ratio, - 'audio_channels': audio_channels, - 'audio_codec': audio_codec, - 'bitrate': bitrate, - 'container': container, - 'height': height, - 'video_codec': video_codec, - 'video_framerate': video_framerate, - 'video_resolution': video_resolution, - 'width': width, - 'ip_address': ip_address, - 'machine_id': machine_id, - 'platform': platform, - 'player': player, - 'transcode_audio_channels': transcode_audio_channels, - 'transcode_audio_codec': transcode_audio_codec, - 'audio_decision': audio_decision, - 'transcode_container': transcode_container, - 'transcode_height': transcode_height, - 'transcode_protocol': transcode_protocol, - 'transcode_video_codec': transcode_video_codec, - 'video_decision': video_decision, - 'transcode_width': transcode_width, - 'transcode_decision': transcode_decision, - 'user_id': user_id, - 'writers': writers, - 'actors': actors, - 'genres': genres, - 'studio': studio, - 'labels': labels - } - - return output - - -def validate_database(database_file=None, table_name=None): - try: - connection = sqlite3.connect(database_file, timeout=20) - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Invalid database specified.") - return 'Invalid database specified.' - except ValueError: - logger.error("Tautulli Importer :: Invalid database specified.") - return 'Invalid database specified.' - except: - logger.error("Tautulli Importer :: Uncaught exception.") - return 'Uncaught exception.' - - try: - connection.execute('SELECT xml from %s' % table_name) - connection.close() - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Invalid database specified.") - return 'Invalid database specified.' - except: - logger.error("Tautulli Importer :: Uncaught exception.") - return 'Uncaught exception.' - - return 'success' - - -def import_from_plexivity(database_file=None, table_name=None, import_ignore_interval=0): - try: - connection = sqlite3.connect(database_file, timeout=20) - connection.row_factory = sqlite3.Row - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Invalid filename.") - return None - except ValueError: - logger.error("Tautulli Importer :: Invalid filename.") - return None - - try: - connection.execute('SELECT xml from %s' % table_name) - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Database specified does not contain the required fields.") - return None - - logger.debug("Tautulli Importer :: Plexivity data import in progress...") - database.set_is_importing(True) - - ap = activity_processor.ActivityProcessor() - user_data = users.Users() - - # Get the latest friends list so we can pull user id's - try: - users.refresh_users() - except: - logger.debug("Tautulli Importer :: Unable to refresh the users list. Aborting import.") - return None - - query = 'SELECT id AS id, ' \ - 'time AS started, ' \ - 'stopped, ' \ - 'null AS user_id, ' \ - 'user, ' \ - 'ip_address, ' \ - 'paused_counter, ' \ - 'platform AS player, ' \ - 'null AS platform, ' \ - 'null as machine_id, ' \ - 'null AS media_type, ' \ - 'null AS view_offset, ' \ - 'xml, ' \ - 'rating as content_rating,' \ - 'summary,' \ - 'title AS full_title,' \ - '(case when orig_title_ep = "n/a" then orig_title else ' \ - 'orig_title_ep end) as title,' \ - '(case when orig_title_ep != "n/a" then orig_title else ' \ - 'null end) as grandparent_title ' \ - 'FROM ' + table_name + ' ORDER BY id' - - result = connection.execute(query) - - for row in result: - # Extract the xml from the Plexivity db xml field. - extracted_xml = extract_plexivity_xml(row['xml']) - - # If we get back None from our xml extractor skip over the record and log error. - if not extracted_xml: - logger.error("Tautulli Importer :: Skipping record with id %s due to malformed xml." - % str(row['id'])) - continue - - # Skip line if we don't have a ratingKey to work with - # if not row['rating_key']: - # logger.error("Tautulli Importer :: Skipping record due to null ratingKey.") - # continue - - # If the user_id no longer exists in the friends list, pull it from the xml. - if user_data.get_user_id(user=row['user']): - user_id = user_data.get_user_id(user=row['user']) - else: - user_id = extracted_xml['user_id'] - - session_history = {'started': arrow.get(row['started']).timestamp, - 'stopped': arrow.get(row['stopped']).timestamp, - 'rating_key': extracted_xml['rating_key'], - 'title': row['title'], - 'parent_title': extracted_xml['parent_title'], - 'grandparent_title': row['grandparent_title'], - 'original_title': extracted_xml['original_title'], - 'full_title': row['full_title'], - 'user_id': user_id, - 'user': row['user'], - 'ip_address': row['ip_address'] if row['ip_address'] else extracted_xml['ip_address'], - 'paused_counter': row['paused_counter'], - 'player': row['player'], - 'platform': extracted_xml['platform'], - 'machine_id': extracted_xml['machine_id'], - 'parent_rating_key': extracted_xml['parent_rating_key'], - 'grandparent_rating_key': extracted_xml['grandparent_rating_key'], - 'media_type': extracted_xml['media_type'], - 'view_offset': extracted_xml['view_offset'], - 'video_decision': extracted_xml['video_decision'], - 'audio_decision': extracted_xml['audio_decision'], - 'transcode_decision': extracted_xml['transcode_decision'], - 'duration': extracted_xml['duration'], - 'width': extracted_xml['width'], - 'height': extracted_xml['height'], - 'container': extracted_xml['container'], - 'video_codec': extracted_xml['video_codec'], - 'audio_codec': extracted_xml['audio_codec'], - 'bitrate': extracted_xml['bitrate'], - 'video_resolution': extracted_xml['video_resolution'], - 'video_framerate': extracted_xml['video_framerate'], - 'aspect_ratio': extracted_xml['aspect_ratio'], - 'audio_channels': extracted_xml['audio_channels'], - 'transcode_protocol': extracted_xml['transcode_protocol'], - 'transcode_container': extracted_xml['transcode_container'], - 'transcode_video_codec': extracted_xml['transcode_video_codec'], - 'transcode_audio_codec': extracted_xml['transcode_audio_codec'], - 'transcode_audio_channels': extracted_xml['transcode_audio_channels'], - 'transcode_width': extracted_xml['transcode_width'], - 'transcode_height': extracted_xml['transcode_height'] - } - - session_history_metadata = {'rating_key': extracted_xml['rating_key'], - 'parent_rating_key': extracted_xml['parent_rating_key'], - 'grandparent_rating_key': extracted_xml['grandparent_rating_key'], - 'title': row['title'], - 'parent_title': extracted_xml['parent_title'], - 'grandparent_title': row['grandparent_title'], - 'original_title': extracted_xml['original_title'], - 'media_index': extracted_xml['media_index'], - 'parent_media_index': extracted_xml['parent_media_index'], - 'thumb': extracted_xml['thumb'], - 'parent_thumb': extracted_xml['parent_thumb'], - 'grandparent_thumb': extracted_xml['grandparent_thumb'], - 'art': extracted_xml['art'], - 'media_type': extracted_xml['media_type'], - 'year': extracted_xml['year'], - 'originally_available_at': extracted_xml['originally_available_at'], - 'added_at': extracted_xml['added_at'], - 'updated_at': extracted_xml['updated_at'], - 'last_viewed_at': extracted_xml['last_viewed_at'], - 'content_rating': row['content_rating'], - 'summary': row['summary'], - 'tagline': extracted_xml['tagline'], - 'rating': extracted_xml['rating'], - 'duration': extracted_xml['duration'], - 'guid': extracted_xml['guid'], - 'section_id': extracted_xml['section_id'], - 'directors': extracted_xml['directors'], - 'writers': extracted_xml['writers'], - 'actors': extracted_xml['actors'], - 'genres': extracted_xml['genres'], - 'studio': extracted_xml['studio'], - 'labels': extracted_xml['labels'], - 'full_title': row['full_title'], - 'width': extracted_xml['width'], - 'height': extracted_xml['height'], - 'container': extracted_xml['container'], - 'video_codec': extracted_xml['video_codec'], - 'audio_codec': extracted_xml['audio_codec'], - 'bitrate': extracted_xml['bitrate'], - 'video_resolution': extracted_xml['video_resolution'], - 'video_framerate': extracted_xml['video_framerate'], - 'aspect_ratio': extracted_xml['aspect_ratio'], - 'audio_channels': extracted_xml['audio_channels'] - } - - # On older versions of PMS, "clip" items were still classified as "movie" and had bad ratingKey values - # Just make sure that the ratingKey is indeed an integer - if session_history_metadata['rating_key'].isdigit(): - ap.write_session_history(session=session_history, - import_metadata=session_history_metadata, - is_import=True, - import_ignore_interval=import_ignore_interval) - else: - logger.debug("Tautulli Importer :: Item has bad rating_key: %s" % session_history_metadata['rating_key']) - - import_users() - - logger.debug("Tautulli Importer :: Plexivity data import complete.") - database.set_is_importing(False) - - -def import_users(): - logger.debug("Tautulli Importer :: Importing Plexivity Users...") - monitor_db = database.MonitorDatabase() - - query = 'INSERT OR IGNORE INTO users (user_id, username) ' \ - 'SELECT user_id, user ' \ - 'FROM session_history WHERE user_id != 1 GROUP BY user_id' - - try: - monitor_db.action(query) - logger.debug("Tautulli Importer :: Users imported.") - except: - logger.debug("Tautulli Importer :: Failed to import users.") diff --git a/jellypy/plexwatch_import.py b/jellypy/plexwatch_import.py deleted file mode 100644 index 79139fdb..00000000 --- a/jellypy/plexwatch_import.py +++ /dev/null @@ -1,448 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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 . - -import sqlite3 -from xml.dom import minidom - -from jellypy import activity_processor -from jellypy import database -from jellypy import helpers -from jellypy import logger -from jellypy import users - - -def extract_plexwatch_xml(xml=None): - output = {} - clean_xml = helpers.latinToAscii(xml) - try: - xml_parse = minidom.parseString(clean_xml) - except: - logger.warn("Tautulli Importer :: Error parsing XML for PlexWatch database.") - return None - - xml_head = xml_parse.getElementsByTagName('opt') - if not xml_head: - logger.warn("Tautulli Importer :: Error parsing XML for PlexWatch database.") - return None - - for a in xml_head: - added_at = helpers.get_xml_attr(a, 'addedAt') - art = helpers.get_xml_attr(a, 'art') - duration = helpers.get_xml_attr(a, 'duration') - grandparent_thumb = helpers.get_xml_attr(a, 'grandparentThumb') - grandparent_title = helpers.get_xml_attr(a, 'grandparentTitle') - original_title = helpers.get_xml_attr(a, 'originalTitle') - guid = helpers.get_xml_attr(a, 'guid') - section_id = helpers.get_xml_attr(a, 'librarySectionID') - media_index = helpers.get_xml_attr(a, 'index') - originally_available_at = helpers.get_xml_attr(a, 'originallyAvailableAt') - last_viewed_at = helpers.get_xml_attr(a, 'lastViewedAt') - parent_media_index = helpers.get_xml_attr(a, 'parentIndex') - parent_thumb = helpers.get_xml_attr(a, 'parentThumb') - rating = helpers.get_xml_attr(a, 'rating') - thumb = helpers.get_xml_attr(a, 'thumb') - media_type = helpers.get_xml_attr(a, 'type') - updated_at = helpers.get_xml_attr(a, 'updatedAt') - view_offset = helpers.get_xml_attr(a, 'viewOffset') - year = helpers.get_xml_attr(a, 'year') - parent_title = helpers.get_xml_attr(a, 'parentTitle') - studio = helpers.get_xml_attr(a, 'studio') - title = helpers.get_xml_attr(a, 'title') - tagline = helpers.get_xml_attr(a, 'tagline') - - directors = [] - if a.getElementsByTagName('Director'): - director_elem = a.getElementsByTagName('Director') - for b in director_elem: - directors.append(helpers.get_xml_attr(b, 'tag')) - - aspect_ratio = '' - audio_channels = None - audio_codec = '' - bitrate = None - container = '' - height = None - video_codec = '' - video_framerate = '' - video_resolution = '' - width = None - - if a.getElementsByTagName('Media'): - media_elem = a.getElementsByTagName('Media') - for c in media_elem: - aspect_ratio = helpers.get_xml_attr(c, 'aspectRatio') - audio_channels = helpers.get_xml_attr(c, 'audioChannels') - audio_codec = helpers.get_xml_attr(c, 'audioCodec') - bitrate = helpers.get_xml_attr(c, 'bitrate') - container = helpers.get_xml_attr(c, 'container') - height = helpers.get_xml_attr(c, 'height') - video_codec = helpers.get_xml_attr(c, 'videoCodec') - video_framerate = helpers.get_xml_attr(c, 'videoFrameRate') - video_resolution = helpers.get_xml_attr(c, 'videoResolution') - width = helpers.get_xml_attr(c, 'width') - - machine_id = '' - platform = '' - player = '' - - if a.getElementsByTagName('Player'): - player_elem = a.getElementsByTagName('Player') - for d in player_elem: - ip_address = helpers.get_xml_attr(d, 'address') - machine_id = helpers.get_xml_attr(d, 'machineIdentifier') - platform = helpers.get_xml_attr(d, 'platform') - player = helpers.get_xml_attr(d, 'title') - - transcode_audio_channels = None - transcode_audio_codec = '' - audio_decision = 'direct play' - transcode_container = '' - transcode_height = None - transcode_protocol = '' - transcode_video_codec = '' - video_decision = 'direct play' - transcode_width = None - - if a.getElementsByTagName('TranscodeSession'): - transcode_elem = a.getElementsByTagName('TranscodeSession') - for e in transcode_elem: - transcode_audio_channels = helpers.get_xml_attr(e, 'audioChannels') - transcode_audio_codec = helpers.get_xml_attr(e, 'audioCodec') - audio_decision = helpers.get_xml_attr(e, 'audioDecision') - transcode_container = helpers.get_xml_attr(e, 'container') - transcode_height = helpers.get_xml_attr(e, 'height') - transcode_protocol = helpers.get_xml_attr(e, 'protocol') - transcode_video_codec = helpers.get_xml_attr(e, 'videoCodec') - video_decision = helpers.get_xml_attr(e, 'videoDecision') - transcode_width = helpers.get_xml_attr(e, 'width') - - # Generate a combined transcode decision value - if video_decision == 'transcode' or audio_decision == 'transcode': - transcode_decision = 'transcode' - elif video_decision == 'copy' or audio_decision == 'copy': - transcode_decision = 'copy' - else: - transcode_decision = 'direct play' - - user_id = None - - if a.getElementsByTagName('User'): - user_elem = a.getElementsByTagName('User') - for f in user_elem: - user_id = helpers.get_xml_attr(f, 'id') - - writers = [] - if a.getElementsByTagName('Writer'): - writer_elem = a.getElementsByTagName('Writer') - for g in writer_elem: - writers.append(helpers.get_xml_attr(g, 'tag')) - - actors = [] - if a.getElementsByTagName('Role'): - actor_elem = a.getElementsByTagName('Role') - for h in actor_elem: - actors.append(helpers.get_xml_attr(h, 'tag')) - - genres = [] - if a.getElementsByTagName('Genre'): - genre_elem = a.getElementsByTagName('Genre') - for i in genre_elem: - genres.append(helpers.get_xml_attr(i, 'tag')) - - labels = [] - if a.getElementsByTagName('Lables'): - label_elem = a.getElementsByTagName('Lables') - for i in label_elem: - labels.append(helpers.get_xml_attr(i, 'tag')) - - output = {'added_at': added_at, - 'art': art, - 'duration': duration, - 'grandparent_thumb': grandparent_thumb, - 'title': title, - 'parent_title': parent_title, - 'grandparent_title': grandparent_title, - 'original_title': original_title, - 'tagline': tagline, - 'guid': guid, - 'section_id': section_id, - 'media_index': media_index, - 'originally_available_at': originally_available_at, - 'last_viewed_at': last_viewed_at, - 'parent_media_index': parent_media_index, - 'parent_thumb': parent_thumb, - 'rating': rating, - 'thumb': thumb, - 'media_type': media_type, - 'updated_at': updated_at, - 'view_offset': view_offset, - 'year': year, - 'directors': directors, - 'aspect_ratio': aspect_ratio, - 'audio_channels': audio_channels, - 'audio_codec': audio_codec, - 'bitrate': bitrate, - 'container': container, - 'height': height, - 'video_codec': video_codec, - 'video_framerate': video_framerate, - 'video_resolution': video_resolution, - 'width': width, - 'ip_address': ip_address, - 'machine_id': machine_id, - 'platform': platform, - 'player': player, - 'transcode_audio_channels': transcode_audio_channels, - 'transcode_audio_codec': transcode_audio_codec, - 'audio_decision': audio_decision, - 'transcode_container': transcode_container, - 'transcode_height': transcode_height, - 'transcode_protocol': transcode_protocol, - 'transcode_video_codec': transcode_video_codec, - 'video_decision': video_decision, - 'transcode_width': transcode_width, - 'transcode_decision': transcode_decision, - 'user_id': user_id, - 'writers': writers, - 'actors': actors, - 'genres': genres, - 'studio': studio, - 'labels': labels - } - - return output - - -def validate_database(database_file=None, table_name=None): - try: - connection = sqlite3.connect(database_file, timeout=20) - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Invalid database specified.") - return 'Invalid database specified.' - except ValueError: - logger.error("Tautulli Importer :: Invalid database specified.") - return 'Invalid database specified.' - except: - logger.error("Tautulli Importer :: Uncaught exception.") - return 'Uncaught exception.' - - try: - connection.execute('SELECT ratingKey from %s' % table_name) - connection.close() - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Invalid database specified.") - return 'Invalid database specified.' - except: - logger.error("Tautulli Importer :: Uncaught exception.") - return 'Uncaught exception.' - - return 'success' - - -def import_from_plexwatch(database_file=None, table_name=None, import_ignore_interval=0): - try: - connection = sqlite3.connect(database_file, timeout=20) - connection.row_factory = sqlite3.Row - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Invalid filename.") - return None - except ValueError: - logger.error("Tautulli Importer :: Invalid filename.") - return None - - try: - connection.execute('SELECT ratingKey from %s' % table_name) - except sqlite3.OperationalError: - logger.error("Tautulli Importer :: Database specified does not contain the required fields.") - return None - - logger.debug("Tautulli Importer :: PlexWatch data import in progress...") - database.set_is_importing(True) - - ap = activity_processor.ActivityProcessor() - user_data = users.Users() - - # Get the latest friends list so we can pull user id's - try: - users.refresh_users() - except: - logger.debug("Tautulli Importer :: Unable to refresh the users list. Aborting import.") - return None - - query = 'SELECT time AS started, ' \ - 'stopped, ' \ - 'cast(ratingKey as text) AS rating_key, ' \ - 'null AS user_id, ' \ - 'user, ' \ - 'ip_address, ' \ - 'paused_counter, ' \ - 'platform AS player, ' \ - 'null AS platform, ' \ - 'null as machine_id, ' \ - 'parentRatingKey as parent_rating_key, ' \ - 'grandparentRatingKey as grandparent_rating_key, ' \ - 'null AS media_type, ' \ - 'null AS view_offset, ' \ - 'xml, ' \ - 'rating as content_rating,' \ - 'summary,' \ - 'title AS full_title,' \ - '(case when orig_title_ep = "" then orig_title else ' \ - 'orig_title_ep end) as title,' \ - '(case when orig_title_ep != "" then orig_title else ' \ - 'null end) as grandparent_title ' \ - 'FROM ' + table_name + ' ORDER BY id' - - result = connection.execute(query) - - for row in result: - # Extract the xml from the Plexwatch db xml field. - extracted_xml = extract_plexwatch_xml(row['xml']) - - # If we get back None from our xml extractor skip over the record and log error. - if not extracted_xml: - logger.error("Tautulli Importer :: Skipping record with ratingKey %s due to malformed xml." - % str(row['rating_key'])) - continue - - # Skip line if we don't have a ratingKey to work with - if not row['rating_key']: - logger.error("Tautulli Importer :: Skipping record due to null ratingKey.") - continue - - # If the user_id no longer exists in the friends list, pull it from the xml. - if user_data.get_user_id(user=row['user']): - user_id = user_data.get_user_id(user=row['user']) - else: - user_id = extracted_xml['user_id'] - - session_history = {'started': row['started'], - 'stopped': row['stopped'], - 'rating_key': row['rating_key'], - 'title': row['title'], - 'parent_title': extracted_xml['parent_title'], - 'grandparent_title': row['grandparent_title'], - 'original_title': extracted_xml['original_title'], - 'full_title': row['full_title'], - 'user_id': user_id, - 'user': row['user'], - 'ip_address': row['ip_address'] if row['ip_address'] else extracted_xml['ip_address'], - 'paused_counter': row['paused_counter'], - 'player': row['player'], - 'platform': extracted_xml['platform'], - 'machine_id': extracted_xml['machine_id'], - 'parent_rating_key': row['parent_rating_key'], - 'grandparent_rating_key': row['grandparent_rating_key'], - 'media_type': extracted_xml['media_type'], - 'view_offset': extracted_xml['view_offset'], - 'video_decision': extracted_xml['video_decision'], - 'audio_decision': extracted_xml['audio_decision'], - 'transcode_decision': extracted_xml['transcode_decision'], - 'duration': extracted_xml['duration'], - 'width': extracted_xml['width'], - 'height': extracted_xml['height'], - 'container': extracted_xml['container'], - 'video_codec': extracted_xml['video_codec'], - 'audio_codec': extracted_xml['audio_codec'], - 'bitrate': extracted_xml['bitrate'], - 'video_resolution': extracted_xml['video_resolution'], - 'video_framerate': extracted_xml['video_framerate'], - 'aspect_ratio': extracted_xml['aspect_ratio'], - 'audio_channels': extracted_xml['audio_channels'], - 'transcode_protocol': extracted_xml['transcode_protocol'], - 'transcode_container': extracted_xml['transcode_container'], - 'transcode_video_codec': extracted_xml['transcode_video_codec'], - 'transcode_audio_codec': extracted_xml['transcode_audio_codec'], - 'transcode_audio_channels': extracted_xml['transcode_audio_channels'], - 'transcode_width': extracted_xml['transcode_width'], - 'transcode_height': extracted_xml['transcode_height'] - } - - session_history_metadata = {'rating_key': helpers.latinToAscii(row['rating_key']), - 'parent_rating_key': row['parent_rating_key'], - 'grandparent_rating_key': row['grandparent_rating_key'], - 'title': row['title'], - 'parent_title': extracted_xml['parent_title'], - 'grandparent_title': row['grandparent_title'], - 'original_title': extracted_xml['original_title'], - 'media_index': extracted_xml['media_index'], - 'parent_media_index': extracted_xml['parent_media_index'], - 'thumb': extracted_xml['thumb'], - 'parent_thumb': extracted_xml['parent_thumb'], - 'grandparent_thumb': extracted_xml['grandparent_thumb'], - 'art': extracted_xml['art'], - 'media_type': extracted_xml['media_type'], - 'year': extracted_xml['year'], - 'originally_available_at': extracted_xml['originally_available_at'], - 'added_at': extracted_xml['added_at'], - 'updated_at': extracted_xml['updated_at'], - 'last_viewed_at': extracted_xml['last_viewed_at'], - 'content_rating': row['content_rating'], - 'summary': row['summary'], - 'tagline': extracted_xml['tagline'], - 'rating': extracted_xml['rating'], - 'duration': extracted_xml['duration'], - 'guid': extracted_xml['guid'], - 'section_id': extracted_xml['section_id'], - 'directors': extracted_xml['directors'], - 'writers': extracted_xml['writers'], - 'actors': extracted_xml['actors'], - 'genres': extracted_xml['genres'], - 'studio': extracted_xml['studio'], - 'labels': extracted_xml['labels'], - 'full_title': row['full_title'], - 'width': extracted_xml['width'], - 'height': extracted_xml['height'], - 'container': extracted_xml['container'], - 'video_codec': extracted_xml['video_codec'], - 'audio_codec': extracted_xml['audio_codec'], - 'bitrate': extracted_xml['bitrate'], - 'video_resolution': extracted_xml['video_resolution'], - 'video_framerate': extracted_xml['video_framerate'], - 'aspect_ratio': extracted_xml['aspect_ratio'], - 'audio_channels': extracted_xml['audio_channels'] - } - - # On older versions of PMS, "clip" items were still classified as "movie" and had bad ratingKey values - # Just make sure that the ratingKey is indeed an integer - if session_history_metadata['rating_key'].isdigit(): - ap.write_session_history(session=session_history, - import_metadata=session_history_metadata, - is_import=True, - import_ignore_interval=import_ignore_interval) - else: - logger.debug("Tautulli Importer :: Item has bad rating_key: %s" % session_history_metadata['rating_key']) - - import_users() - - logger.debug("Tautulli Importer :: PlexWatch data import complete.") - database.set_is_importing(False) - - -def import_users(): - logger.debug("Tautulli Importer :: Importing PlexWatch Users...") - monitor_db = database.MonitorDatabase() - - query = 'INSERT OR IGNORE INTO users (user_id, username) ' \ - 'SELECT user_id, user ' \ - 'FROM session_history WHERE user_id != 1 GROUP BY user_id' - - try: - monitor_db.action(query) - logger.debug("Tautulli Importer :: Users imported.") - except: - logger.debug("Tautulli Importer :: Failed to import users.") diff --git a/jellypy/webauth.py b/jellypy/webauth.py index 3c03e9d0..efd36d0b 100644 --- a/jellypy/webauth.py +++ b/jellypy/webauth.py @@ -26,12 +26,12 @@ from urllib.parse import quote, unquote import cherrypy import jwt -from hashing_passwords import check_hash import jellypy from jellypy import logger from jellypy.database import MonitorDatabase from jellypy.helpers import timestamp +from jellypy.password import check_hash from jellypy.users import Users, refresh_users from jellypy.plextv import PlexTV @@ -44,7 +44,7 @@ except ImportError: Morsel._reserved[str('samesite')] = str('SameSite') JWT_ALGORITHM = 'HS256' -JWT_COOKIE_NAME = 'tautulli_token_' +JWT_COOKIE_NAME = 'jellypy_token_' def plex_user_login(username=None, password=None, token=None, headers=None): diff --git a/jellypy/webserve.py b/jellypy/webserve.py index a85e1953..7837a254 100644 --- a/jellypy/webserve.py +++ b/jellypy/webserve.py @@ -25,6 +25,7 @@ import sys import threading import zipfile from io import open, BytesIO +from urllib.parse import urlencode import cherrypy import mako.exceptions @@ -32,7 +33,6 @@ import mako.template import websocket from cherrypy import NotFound from cherrypy.lib.static import serve_file, serve_fileobj, serve_download -from hashing_passwords import make_hash from mako.lookup import TemplateLookup import jellypy @@ -48,14 +48,12 @@ from jellypy import http_handler from jellypy import libraries from jellypy import log_reader from jellypy import logger +from jellypy import mobile_app from jellypy import newsletter_handler from jellypy import newsletters -from jellypy import mobile_app from jellypy import notification_handler from jellypy import notifiers from jellypy import plextv -from jellypy import plexivity_import -from jellypy import plexwatch_import from jellypy import pmsconnect from jellypy import users from jellypy import versioncheck @@ -63,6 +61,7 @@ from jellypy import web_socket from jellypy import webstart from jellypy.api2 import API2 from jellypy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out +from jellypy.password import make_hash from jellypy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library from jellypy.webauth import AuthController, requireAuth, member_of, check_auth @@ -3881,37 +3880,6 @@ class WebInterface(object): if database_file: helpers.delete_file(database_path) return {'result': 'error', 'message': db_check_msg} - - elif app.lower() == 'plexwatch': - db_check_msg = plexwatch_import.validate_database(database_file=database_path, - table_name=table_name) - if db_check_msg == 'success': - threading.Thread(target=plexwatch_import.import_from_plexwatch, - kwargs={'database_file': database_path, - 'table_name': table_name, - 'import_ignore_interval': import_ignore_interval}).start() - return {'result': 'success', - 'message': 'Database import has started. Check the logs to monitor any problems.'} - else: - if database_file: - helpers.delete_file(database_path) - return {'result': 'error', 'message': db_check_msg} - - elif app.lower() == 'plexivity': - db_check_msg = plexivity_import.validate_database(database_file=database_path, - table_name=table_name) - if db_check_msg == 'success': - threading.Thread(target=plexivity_import.import_from_plexivity, - kwargs={'database_file': database_path, - 'table_name': table_name, - 'import_ignore_interval': import_ignore_interval}).start() - return {'result': 'success', - 'message': 'Database import has started. Check the logs to monitor any problems.'} - else: - if database_file: - helpers.delete_file(database_path) - return {'result': 'error', 'message': db_check_msg} - else: return {'result': 'error', 'message': 'App not recognized for import'} diff --git a/jellypy/windows.py b/jellypy/windows.py deleted file mode 100644 index 6b709c9b..00000000 --- a/jellypy/windows.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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 . - -import os -import sys -from systray import SysTrayIcon - -try: - from shlex import quote as cmd_quote -except ImportError: - from pipes import quote as cmd_quote - -try: - import winreg -except ImportError: - import _winreg as winreg - -import jellypy -if jellypy.PYTHON2: - import common - import logger - import versioncheck -else: - from jellypy import common - from jellypy import logger - from jellypy import versioncheck - - -class WindowsSystemTray(object): - def __init__(self): - self.image_dir = os.path.join(jellypy.PROG_DIR, 'data/interfaces/', jellypy.CONFIG.INTERFACE, 'images') - self.icon = os.path.join(self.image_dir, 'logo-circle.ico') - - if jellypy.UPDATE_AVAILABLE: - self.hover_text = common.PRODUCT + ' - Update Available!' - self.update_title = 'Check for Updates - Update Available!' - else: - self.hover_text = common.PRODUCT - self.update_title = 'Check for Updates' - - if jellypy.CONFIG.LAUNCH_STARTUP: - launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico') - else: - launch_start_icon = None - if jellypy.CONFIG.LAUNCH_BROWSER: - launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico') - else: - launch_browser_icon = None - - self.menu = [ - ['Open Tautulli', None, self.tray_open, 'default'], - ['', None, 'separator', None], - ['Start Tautulli at Login', launch_start_icon, self.tray_startup, None], - ['Open Browser when Tautulli Starts', launch_browser_icon, self.tray_browser, None], - ['', None, 'separator', None], - [self.update_title, None, self.tray_check_update, None], - ['Restart', None, self.tray_restart, None] - ] - if not jellypy.FROZEN: - self.menu.insert(6, ['Update', None, self.tray_update, None]) - - self.tray_icon = SysTrayIcon(self.icon, self.hover_text, self.menu, on_quit=self.tray_quit) - - def start(self): - logger.info("Launching Windows system tray icon.") - try: - self.tray_icon.start() - except Exception as e: - logger.error("Unable to launch system tray icon: %s." % e) - - def shutdown(self): - self.tray_icon.shutdown() - - def update(self, **kwargs): - self.tray_icon.update(**kwargs) - - def tray_open(self, tray_icon): - jellypy.launch_browser(jellypy.CONFIG.HTTP_HOST, jellypy.HTTP_PORT, jellypy.HTTP_ROOT) - - def tray_startup(self, tray_icon): - jellypy.CONFIG.LAUNCH_STARTUP = not jellypy.CONFIG.LAUNCH_STARTUP - set_startup() - - def tray_browser(self, tray_icon): - jellypy.CONFIG.LAUNCH_BROWSER = not jellypy.CONFIG.LAUNCH_BROWSER - set_startup() - - def tray_check_update(self, tray_icon): - versioncheck.check_update() - - def tray_update(self, tray_icon): - if jellypy.UPDATE_AVAILABLE: - jellypy.SIGNAL = 'update' - else: - self.hover_text = common.PRODUCT + ' - No Update Available' - self.update_title = 'Check for Updates - No Update Available' - self.menu[5][0] = self.update_title - self.update(hover_text=self.hover_text, menu_options=self.menu) - - def tray_restart(self, tray_icon): - jellypy.SIGNAL = 'restart' - - def tray_quit(self, tray_icon): - jellypy.SIGNAL = 'shutdown' - - def change_tray_update_icon(self): - if jellypy.UPDATE_AVAILABLE: - self.hover_text = common.PRODUCT + ' - Update Available!' - self.update_title = 'Check for Updates - Update Available!' - else: - self.hover_text = common.PRODUCT + ' - No Update Available' - self.update_title = 'Check for Updates' - self.menu[5][0] = self.update_title - self.update(hover_text=self.hover_text, menu_options=self.menu) - - def change_tray_icons(self): - if jellypy.CONFIG.LAUNCH_STARTUP: - launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico') - else: - launch_start_icon = None - if jellypy.CONFIG.LAUNCH_BROWSER: - launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico') - else: - launch_browser_icon = None - self.menu[2][1] = launch_start_icon - self.menu[3][1] = launch_browser_icon - self.update(menu_options=self.menu) - - -def set_startup(): - if jellypy.WIN_SYS_TRAY_ICON: - jellypy.WIN_SYS_TRAY_ICON.change_tray_icons() - - startup_reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run" - - exe = sys.executable - run_args = [arg for arg in jellypy.ARGS if arg != '--nolaunch'] - if jellypy.FROZEN: - args = [exe] + run_args - else: - args = [exe, jellypy.FULL_PATH] + run_args - - registry_key_name = '{}_{}'.format(common.PRODUCT, jellypy.CONFIG.PMS_UUID) - - cmd = ' '.join(cmd_quote(arg) for arg in args).replace('python.exe', 'pythonw.exe').replace("'", '"') - - if jellypy.CONFIG.LAUNCH_STARTUP: - # Rename old Tautulli registry key - try: - registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS) - winreg.QueryValueEx(registry_key, common.PRODUCT) - reg_value_exists = True - except WindowsError: - reg_value_exists = False - - if reg_value_exists: - try: - winreg.DeleteValue(registry_key, common.PRODUCT) - winreg.CloseKey(registry_key) - except WindowsError: - pass - - try: - winreg.CreateKey(winreg.HKEY_CURRENT_USER, startup_reg_path) - registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_WRITE) - winreg.SetValueEx(registry_key, registry_key_name, 0, winreg.REG_SZ, cmd) - winreg.CloseKey(registry_key) - logger.info("Added Tautulli to Windows system startup registry key.") - return True - except WindowsError as e: - logger.error("Failed to create Windows system startup registry key: %s", e) - return False - - else: - # Check if registry value exists - try: - registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS) - winreg.QueryValueEx(registry_key, registry_key_name) - reg_value_exists = True - except WindowsError: - reg_value_exists = False - - if reg_value_exists: - try: - winreg.DeleteValue(registry_key, registry_key_name) - winreg.CloseKey(registry_key) - logger.info("Removed Tautulli from Windows system startup registry key.") - return True - except WindowsError as e: - logger.error("Failed to delete Windows system startup registry key: %s", e) - return False