From 91e28df1a5b8e0e49b7864b5df4fd5835a708a51 Mon Sep 17 00:00:00 2001
From: Giovanni Harting <539@idlegandalf.com>
Date: Fri, 5 Feb 2021 18:16:12 +0100
Subject: [PATCH] Removed some facebook stuff, maybe adding it back later
---
jellypy/__init__.py | 1 +
jellypy/api2.py | 2 +-
jellypy/certgen.py | 110 +++++++++
jellypy/classes.py | 66 ------
jellypy/helpers.py | 3 +-
jellypy/notifiers.py | 337 ++++++--------------------
jellypy/password.py | 22 ++
jellypy/plex.py | 8 -
jellypy/plexivity_import.py | 456 ------------------------------------
jellypy/plexwatch_import.py | 448 -----------------------------------
jellypy/webauth.py | 4 +-
jellypy/webserve.py | 38 +--
jellypy/windows.py | 205 ----------------
13 files changed, 219 insertions(+), 1481 deletions(-)
create mode 100644 jellypy/certgen.py
delete mode 100644 jellypy/classes.py
create mode 100644 jellypy/password.py
delete mode 100644 jellypy/plexivity_import.py
delete mode 100644 jellypy/plexwatch_import.py
delete mode 100644 jellypy/windows.py
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