Removed some facebook stuff, maybe adding it back later
This commit is contained in:
@@ -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
|
||||||
|
@@ -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
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
|
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
|
||||||
|
@@ -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'],
|
||||||
@@ -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'}
|
||||||
|
|
||||||
@@ -991,7 +978,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
'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:
|
||||||
@@ -1003,7 +990,8 @@ class ANDROIDAPP(Notifier):
|
|||||||
})
|
})
|
||||||
|
|
||||||
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(
|
||||||
|
'https://onesignal.com') + '" target="_blank">' \
|
||||||
'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \
|
'OneSignal</a>. Some user data is collected and cannot be encrypted. ' \
|
||||||
'Please read the <a href="' + helpers.anon_url(
|
'Please read the <a href="' + helpers.anon_url(
|
||||||
'https://onesignal.com/privacy_policy') + '" target="_blank">' \
|
'https://onesignal.com/privacy_policy') + '" target="_blank">' \
|
||||||
@@ -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,7 +1614,8 @@ 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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
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
|
# 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):
|
||||||
|
@@ -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 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):
|
||||||
|
@@ -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'}
|
||||||
|
|
||||||
|
@@ -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