Removed some facebook stuff, maybe adding it back later
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import queue
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
|
@@ -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):
|
||||
|
110
jellypy/certgen.py
Normal file
110
jellypy/certgen.py
Normal 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
|
@@ -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)
|
@@ -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
|
||||
|
@@ -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 '
|
||||
'<a href="' + helpers.anon_url(
|
||||
'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
|
||||
% (jellypy.CONFIG.GIT_USER, jellypy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.' ,
|
||||
'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
|
||||
% (jellypy.CONFIG.GIT_USER, jellypy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.',
|
||||
'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'] += '<br><br>Notifications are sent using the ' \
|
||||
'<a href="' + helpers.anon_url('https://onesignal.com') + '" target="_blank">' \
|
||||
'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \
|
||||
'Please read the <a href="' + helpers.anon_url(
|
||||
'https://onesignal.com/privacy_policy') + '" target="_blank">' \
|
||||
'OneSignal Privacy Policy</a> for more details.'
|
||||
'<a href="' + helpers.anon_url(
|
||||
'https://onesignal.com') + '" target="_blank">' \
|
||||
'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \
|
||||
'Please read the <a href="' + helpers.anon_url(
|
||||
'https://onesignal.com/privacy_policy') + '" target="_blank">' \
|
||||
'OneSignal Privacy Policy</a> for more details.'
|
||||
|
||||
devices = self.get_devices()
|
||||
|
||||
@@ -1018,7 +1006,7 @@ class ANDROIDAPP(Notifier):
|
||||
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
|
||||
'Get the Android App</a> and register a device.',
|
||||
'input_type': 'help'
|
||||
})
|
||||
})
|
||||
else:
|
||||
config_option.append({
|
||||
'label': 'Device',
|
||||
@@ -1029,7 +1017,7 @@ class ANDROIDAPP(Notifier):
|
||||
'register a new device</a> 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.<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):
|
||||
"""
|
||||
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'
|
||||
' <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'
|
||||
},
|
||||
{'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': '<span class="inline-pre">' + \
|
||||
', '.join(self.script_exts) + '</span>',
|
||||
', '.join(self.script_exts) + '</span>',
|
||||
'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)
|
||||
|
22
jellypy/password.py
Normal file
22
jellypy/password.py
Normal 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
|
@@ -15,16 +15,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import 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):
|
||||
|
@@ -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.")
|
@@ -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.")
|
@@ -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):
|
||||
|
@@ -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'}
|
||||
|
||||
|
@@ -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
|
Reference in New Issue
Block a user