Removed some facebook stuff, maybe adding it back later

This commit is contained in:
2021-02-05 18:16:12 +01:00
parent 1df28243c3
commit 91e28df1a5
13 changed files with 219 additions and 1481 deletions

View File

@@ -15,6 +15,7 @@
import datetime import datetime
import os import os
import queue
import sqlite3 import sqlite3
import subprocess import subprocess
import sys import sys

View File

@@ -27,7 +27,6 @@ from io import open
import cherrypy import cherrypy
import xmltodict import xmltodict
from hashing_passwords import check_hash
import jellypy import jellypy
from jellypy import common from jellypy import common
@@ -43,6 +42,7 @@ from jellypy import newsletter_handler
from jellypy import newsletters from jellypy import newsletters
from jellypy import plextv from jellypy import plextv
from jellypy import users from jellypy import users
from jellypy.password import check_hash
class API2(object): class API2(object):

110
jellypy/certgen.py Normal file
View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
#########################################
## 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)

View File

@@ -32,6 +32,8 @@ import time
import unicodedata import unicodedata
from collections import OrderedDict from collections import OrderedDict
from functools import reduce, wraps from functools import reduce, wraps
from itertools import zip_longest, islice
from urllib.parse import urlencode
from xml.dom import minidom from xml.dom import minidom
import arrow import arrow
@@ -46,7 +48,6 @@ from cloudinary.uploader import upload
from cloudinary.utils import cloudinary_url from cloudinary.utils import cloudinary_url
import jellypy import jellypy
from jellypy import common from jellypy import common
from jellypy import logger from jellypy import logger
from jellypy import request from jellypy import request

View File

@@ -17,26 +17,29 @@
import base64 import base64
import bleach
import json
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import email.utils import email.utils
from paho.mqtt.publish import single import json
import os import os
import re import re
import requests
import smtplib import smtplib
import subprocess import subprocess
import sys import sys
import threading import threading
import time 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: try:
from Cryptodome.Protocol.KDF import PBKDF2 from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Cipher import AES from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes from Cryptodome.Random import get_random_bytes
from Cryptodome.Hash import HMAC, SHA1 from Cryptodome.Hash import HMAC, SHA1
CRYPTODOME = True CRYPTODOME = True
except ImportError: except ImportError:
try: try:
@@ -44,34 +47,23 @@ except ImportError:
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes from Crypto.Random import get_random_bytes
from Crypto.Hash import HMAC, SHA1 from Crypto.Hash import HMAC, SHA1
CRYPTODOME = True CRYPTODOME = True
except ImportError: except ImportError:
CRYPTODOME = False CRYPTODOME = False
import gntp.notifier import gntp.notifier
import facebook
import twitter import twitter
import jellypy import jellypy
if jellypy.PYTHON2: from jellypy import common
import common from jellypy import database
import database from jellypy import helpers
import helpers from jellypy import logger
import logger from jellypy import mobile_app
import mobile_app from jellypy import pmsconnect
import pmsconnect from jellypy import request
import request from jellypy import users
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
BROWSER_NOTIFIERS = {} BROWSER_NOTIFIERS = {}
@@ -135,12 +127,6 @@ def available_notification_agents():
'class': EMAIL, 'class': EMAIL,
'action_types': ('all',) 'action_types': ('all',)
}, },
{'label': 'Facebook',
'name': 'facebook',
'id': AGENT_IDS['facebook'],
'class': FACEBOOK,
'action_types': ('all',)
},
{'label': 'GroupMe', {'label': 'GroupMe',
'name': 'groupme', 'name': 'groupme',
'id': AGENT_IDS['groupme'], 'id': AGENT_IDS['groupme'],
@@ -788,7 +774,7 @@ class PrettyMetadata(object):
@staticmethod @staticmethod
def get_parameters(): def get_parameters():
parameters = {param['value']: param['name'] 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[''] = '' parameters[''] = ''
return parameters return parameters
@@ -852,7 +838,8 @@ class Notifier(object):
if response is not None and 400 <= response.status_code < 500: if response is not None and 400 <= response.status_code < 500:
verify_msg = " Verify your notification agent settings are correct." 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: if err_msg:
logger.error("Tautulli Notifiers :: {}".format(err_msg)) logger.error("Tautulli Notifiers :: {}".format(err_msg))
@@ -907,7 +894,7 @@ class ANDROIDAPP(Notifier):
'rating_key': pretty_metadata.parameters.get('rating_key', ''), 'rating_key': pretty_metadata.parameters.get('rating_key', ''),
'poster_thumb': pretty_metadata.parameters.get('poster_thumb', '')} 'poster_thumb': pretty_metadata.parameters.get('poster_thumb', '')}
#logger.debug("Plaintext data: {}".format(plaintext_data)) # logger.debug("Plaintext data: {}".format(plaintext_data))
if CRYPTODOME: if CRYPTODOME:
# Key generation # Key generation
@@ -918,7 +905,7 @@ class ANDROIDAPP(Notifier):
key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations, key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations,
prf=lambda p, s: HMAC.new(p, s, SHA1).digest()) 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 # Encrypt using AES GCM
nonce = get_random_bytes(16) 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 = cipher.encrypt_and_digest(json.dumps(plaintext_data).encode('utf-8'))
encrypted_data += gcm_tag encrypted_data += gcm_tag
#logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data))) # logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data)))
#logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag))) # logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag)))
#logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce))) # logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
#logger.debug("Salt (base64): {}".format(base64.b64encode(salt))) # logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
payload = {'app_id': mobile_app._ONESIGNAL_APP_ID, payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
'include_player_ids': [device['onesignal_id']], 'include_player_ids': [device['onesignal_id']],
@@ -953,7 +940,7 @@ class ANDROIDAPP(Notifier):
'server_id': jellypy.CONFIG.PMS_UUID} 'server_id': jellypy.CONFIG.PMS_UUID}
} }
#logger.debug("OneSignal payload: {}".format(payload)) # logger.debug("OneSignal payload: {}".format(payload))
headers = {'Content-Type': 'application/json'} headers = {'Content-Type': 'application/json'}
@@ -990,24 +977,25 @@ class ANDROIDAPP(Notifier):
'Please install the library to encrypt the notification contents. ' 'Please install the library to encrypt the notification contents. '
'Instructions can be found in the ' 'Instructions can be found in the '
'<a href="' + helpers.anon_url( '<a href="' + helpers.anon_url(
'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome' 'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
% (jellypy.CONFIG.GIT_USER, jellypy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.' , % (jellypy.CONFIG.GIT_USER, jellypy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.',
'input_type': 'help' 'input_type': 'help'
}) })
else: else:
config_option.append({ config_option.append({
'label': 'Note', 'label': 'Note',
'description': 'The PyCryptodome library was found. ' 'description': 'The PyCryptodome library was found. '
'The content of your notifications will be sent encrypted!', 'The content of your notifications will be sent encrypted!',
'input_type': 'help' 'input_type': 'help'
}) })
config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \ config_option[-1]['description'] += '<br><br>Notifications are sent using the ' \
'<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \ '<a href="' + helpers.anon_url(
'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \ 'https://onesignal.com') + '" target="_blank">' \
'Please read the <a href="' + helpers.anon_url( 'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \
'https://onesignal.com/privacy_policy') + '" target="_blank">' \ 'Please read the <a href="' + helpers.anon_url(
'OneSignal Privacy Policy</a> for more details.' 'https://onesignal.com/privacy_policy') + '" target="_blank">' \
'OneSignal Privacy Policy</a> for more details.'
devices = self.get_devices() devices = self.get_devices()
@@ -1018,7 +1006,7 @@ class ANDROIDAPP(Notifier):
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">' '<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
'Get the Android App</a> and register a device.', 'Get the Android App</a> and register a device.',
'input_type': 'help' 'input_type': 'help'
}) })
else: else:
config_option.append({ config_option.append({
'label': 'Device', 'label': 'Device',
@@ -1029,7 +1017,7 @@ class ANDROIDAPP(Notifier):
'register a new device</a> with Tautulli.', 'register a new device</a> with Tautulli.',
'input_type': 'select', 'input_type': 'select',
'select_options': devices 'select_options': devices
}) })
config_option.append({ config_option.append({
'label': 'Priority', 'label': 'Priority',
@@ -1038,7 +1026,7 @@ class ANDROIDAPP(Notifier):
'description': 'Set the notification priority.', 'description': 'Set the notification priority.',
'input_type': 'select', 'input_type': 'select',
'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'} 'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'}
}) })
return config_option return config_option
@@ -1081,7 +1069,7 @@ class BOXCAR(Notifier):
'flourish': 'Flourish', 'flourish': 'Flourish',
'harp': 'Harp', 'harp': 'Harp',
'light': 'Light', 'light': 'Light',
'magic-chime':'Magic Chime', 'magic-chime': 'Magic Chime',
'magic-coin': 'Magic Coin', 'magic-coin': 'Magic Coin',
'no-sound': 'No Sound', 'no-sound': 'No Sound',
'notifier-1': 'Notifier (1)', 'notifier-1': 'Notifier (1)',
@@ -1507,191 +1495,6 @@ class EMAIL(Notifier):
return config_option 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.<br>'
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'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.<br>'
'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.<br>'
'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): class GROUPME(Notifier):
""" """
GroupMe notifications GroupMe notifications
@@ -1715,7 +1518,7 @@ class GROUPME(Notifier):
pretty_metadata = PrettyMetadata(kwargs.get('parameters')) pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
# Retrieve the poster from Plex # 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]: if result and result[0]:
poster_content = result[0] poster_content = result[0]
else: else:
@@ -1811,12 +1614,13 @@ class GROWL(Notifier):
logger.error("Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME)) logger.error("Tautulli Notifiers :: {name} notification failed: network error".format(name=self.NAME))
return False return False
except gntp.notifier.errors.AuthError: 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 return False
# Send it, including an image # Send it, including an image
image_file = os.path.join(str(jellypy.PROG_DIR), 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: with open(image_file, 'rb') as f:
image = f.read() image = f.read()
@@ -1882,7 +1686,8 @@ class IFTTT(Notifier):
'value': self.config['key'], 'value': self.config['key'],
'name': 'ifttt_key', 'name': 'ifttt_key',
'description': 'Your IFTTT webhook key. You can get a key from' 'description': 'Your IFTTT webhook key. You can get a key from'
' <a href="' + helpers.anon_url('https://ifttt.com/maker_webhooks') + '" target="_blank">here</a>.', ' <a href="' + helpers.anon_url(
'https://ifttt.com/maker_webhooks') + '" target="_blank">here</a>.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'IFTTT Event', {'label': 'IFTTT Event',
@@ -1961,10 +1766,13 @@ class JOIN(Notifier):
return True return True
else: else:
error_msg = response_data.get('errorMessage') 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 return False
else: 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))) logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
return False return False
@@ -1985,14 +1793,19 @@ class JOIN(Notifier):
devices.update({d['deviceName']: d['deviceName'] for d in response_devices}) devices.update({d['deviceName']: d['deviceName'] for d in response_devices})
else: else:
error_msg = response_data.get('errorMessage') 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: 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))) logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
except Exception as e: 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 return devices
@@ -2092,7 +1905,8 @@ class MQTT(Notifier):
if self.config['password']: if self.config['password']:
auth['password'] = 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'], 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']) 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): def wrapper(self, *args, **kwargs):
return func(self, old_IMP, *args, **kwargs) return func(self, old_IMP, *args, **kwargs)
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector, new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
signature=old_IMP.signature) signature=old_IMP.signature)
self.objc.classAddMethod(cls, SEL, new_IMP) self.objc.classAddMethod(cls, SEL, new_IMP)
@@ -2217,8 +2032,8 @@ class OSX(Notifier):
try: try:
self._swizzle(self.objc.lookUpClass('NSBundle'), self._swizzle(self.objc.lookUpClass('NSBundle'),
b'bundleIdentifier', b'bundleIdentifier',
self._swizzled_bundleIdentifier) self._swizzled_bundleIdentifier)
NSUserNotification = self.objc.lookUpClass('NSUserNotification') NSUserNotification = self.objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') 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")) image = os.path.join(jellypy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))
for host in hosts: 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: 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 if version < 12: # Eden
notification = subject + "," + body + "," + str(display_time) notification = subject + "," + body + "," + str(display_time)
@@ -2410,7 +2227,7 @@ class PLEXMOBILEAPP(Notifier):
if action == 'test': if action == 'test':
tests = [] tests = []
for configuration in self.configurations: 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) return all(tests)
configuration_action = action.split('test_')[-1] 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))) logger.debug("Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
except Exception as e: 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 return devices
@@ -3016,7 +2834,7 @@ class SCRIPTS(Notifier):
'TAUTULLI_APIKEY': jellypy.CONFIG.API_KEY, 'TAUTULLI_APIKEY': jellypy.CONFIG.API_KEY,
'TAUTULLI_ENCODING': jellypy.SYS_ENCODING, 'TAUTULLI_ENCODING': jellypy.SYS_ENCODING,
'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION 'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
} }
if user_id: if user_id:
user_tokens = users.Users().get_tokens(user_id=user_id) user_tokens = users.Users().get_tokens(user_id=user_id)
@@ -3144,7 +2962,7 @@ class SCRIPTS(Notifier):
def _return_config_options(self): def _return_config_options(self):
config_option = [{'label': 'Supported File Types', config_option = [{'label': 'Supported File Types',
'description': '<span class="inline-pre">' + \ 'description': '<span class="inline-pre">' + \
', '.join(self.script_exts) + '</span>', ', '.join(self.script_exts) + '</span>',
'input_type': 'help' 'input_type': 'help'
}, },
{'label': 'Script Folder', {'label': 'Script Folder',
@@ -3518,7 +3336,7 @@ class TWITTER(Notifier):
poster_url = '' poster_url = ''
if self.config['incl_poster'] and kwargs.get('parameters'): if self.config['incl_poster'] and kwargs.get('parameters'):
parameters = kwargs['parameters'] parameters = kwargs['parameters']
poster_url = parameters.get('poster_url','') poster_url = parameters.get('poster_url', '')
# Hack to add media type to attachment # Hack to add media type to attachment
if poster_url and not helpers.get_img_service(): if poster_url and not helpers.get_img_service():
@@ -3688,7 +3506,8 @@ class XBMC(Notifier):
for host in hosts: for host in hosts:
logger.info("Tautulli Notifiers :: Sending notification command to XMBC @ " + host) logger.info("Tautulli Notifiers :: Sending notification command to XMBC @ " + host)
try: 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 if version < 12: # Eden
notification = subject + "," + body + "," + str(display_time) notification = subject + "," + body + "," + str(display_time)

22
jellypy/password.py Normal file
View File

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

View File

@@ -15,16 +15,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>. # along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from plexapi.server import PlexServer from plexapi.server import PlexServer
import jellypy
if jellypy.PYTHON2:
import logger
else:
from jellypy import logger
class Plex(object): class Plex(object):
def __init__(self, url, token): def __init__(self, url, token):

View File

@@ -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 <http://www.gnu.org/licenses/>.
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.")

View File

@@ -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 <http://www.gnu.org/licenses/>.
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.")

View File

@@ -26,12 +26,12 @@ from urllib.parse import quote, unquote
import cherrypy import cherrypy
import jwt import jwt
from hashing_passwords import check_hash
import jellypy import jellypy
from jellypy import logger from jellypy import logger
from jellypy.database import MonitorDatabase from jellypy.database import MonitorDatabase
from jellypy.helpers import timestamp from jellypy.helpers import timestamp
from jellypy.password import check_hash
from jellypy.users import Users, refresh_users from jellypy.users import Users, refresh_users
from jellypy.plextv import PlexTV from jellypy.plextv import PlexTV
@@ -44,7 +44,7 @@ except ImportError:
Morsel._reserved[str('samesite')] = str('SameSite') Morsel._reserved[str('samesite')] = str('SameSite')
JWT_ALGORITHM = 'HS256' 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): def plex_user_login(username=None, password=None, token=None, headers=None):

View File

@@ -25,6 +25,7 @@ import sys
import threading import threading
import zipfile import zipfile
from io import open, BytesIO from io import open, BytesIO
from urllib.parse import urlencode
import cherrypy import cherrypy
import mako.exceptions import mako.exceptions
@@ -32,7 +33,6 @@ import mako.template
import websocket import websocket
from cherrypy import NotFound from cherrypy import NotFound
from cherrypy.lib.static import serve_file, serve_fileobj, serve_download from cherrypy.lib.static import serve_file, serve_fileobj, serve_download
from hashing_passwords import make_hash
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
import jellypy import jellypy
@@ -48,14 +48,12 @@ from jellypy import http_handler
from jellypy import libraries from jellypy import libraries
from jellypy import log_reader from jellypy import log_reader
from jellypy import logger from jellypy import logger
from jellypy import mobile_app
from jellypy import newsletter_handler from jellypy import newsletter_handler
from jellypy import newsletters from jellypy import newsletters
from jellypy import mobile_app
from jellypy import notification_handler from jellypy import notification_handler
from jellypy import notifiers from jellypy import notifiers
from jellypy import plextv from jellypy import plextv
from jellypy import plexivity_import
from jellypy import plexwatch_import
from jellypy import pmsconnect from jellypy import pmsconnect
from jellypy import users from jellypy import users
from jellypy import versioncheck from jellypy import versioncheck
@@ -63,6 +61,7 @@ from jellypy import web_socket
from jellypy import webstart from jellypy import webstart
from jellypy.api2 import API2 from jellypy.api2 import API2
from jellypy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out 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.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 from jellypy.webauth import AuthController, requireAuth, member_of, check_auth
@@ -3881,37 +3880,6 @@ class WebInterface(object):
if database_file: if database_file:
helpers.delete_file(database_path) helpers.delete_file(database_path)
return {'result': 'error', 'message': db_check_msg} 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: else:
return {'result': 'error', 'message': 'App not recognized for import'} return {'result': 'error', 'message': 'App not recognized for import'}

View File

@@ -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 <http://www.gnu.org/licenses/>.
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