Allow remote applications to interface with Tautulli.
+
Allow external applications to interface with Tautulli.
@@ -1559,20 +1559,28 @@
-
+
-
Tautulli Remote Android App
+
Tautulli Remote App
- Get the Tautulli Remote app on Google PlayTM to access Tautulli from your Android device!
-
-
+ Get the Tautulli Remote app on
+ Google Play
+ to access Tautulli from your mobile device!
+
+
+
+
+
+
+ Google Play and the Google Play logo are trademarks of Google LLC.
- Scan the QR code below with the Tautulli Android app to automatically register it with the server (make sure the Tautulli Address below is correct)
+ Scan the QR code below with the Tautulli Remote app to automatically register it with the server (make sure the Tautulli Address below is correct)
or manually enter the connection info and device token into the app settings. This window will automatically close once device registration is successful.
diff --git a/plexpy/__init__.py b/plexpy/__init__.py
index ffe3f0d9..b2606f9d 100644
--- a/plexpy/__init__.py
+++ b/plexpy/__init__.py
@@ -1,4 +1,4 @@
-# This file is part of Tautulli.
+# 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
@@ -2342,7 +2342,9 @@ def dbcheck():
# Rename notifiers in the database
result = c_db.execute('SELECT agent_label FROM notifiers '
- 'WHERE agent_label = "XBMC" OR agent_label = "OSX Notify"').fetchone()
+ 'WHERE agent_label = "XBMC" '
+ 'OR agent_label = "OSX Notify" '
+ 'OR agent_name = "androidapp"').fetchone()
if result:
logger.debug("Altering database. Renaming notifiers.")
c_db.execute(
@@ -2351,6 +2353,10 @@ def dbcheck():
c_db.execute(
'UPDATE notifiers SET agent_label = "macOS Notification Center" WHERE agent_label = "OSX Notify"'
)
+ c_db.execute(
+ 'UPDATE notifiers SET agent_name = "remoteapp", agent_label = "Tautulli Remote App" '
+ 'WHERE agent_name = "androidapp"'
+ )
# Upgrade exports table from earlier versions
try:
diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py
index 5fce3beb..57334703 100644
--- a/plexpy/notifiers.py
+++ b/plexpy/notifiers.py
@@ -98,7 +98,7 @@ AGENT_IDS = {'growl': 0,
'browser': 17,
'join': 18,
'discord': 20,
- 'androidapp': 21,
+ 'remoteapp': 21,
'groupme': 22,
'mqtt': 23,
'zapier': 24,
@@ -110,10 +110,10 @@ DEFAULT_CUSTOM_CONDITIONS = [{'parameter': '', 'operator': '', 'value': ''}]
def available_notification_agents():
- agents = [{'label': 'Tautulli Remote Android App',
- 'name': 'androidapp',
- 'id': AGENT_IDS['androidapp'],
- 'class': ANDROIDAPP,
+ agents = [{'label': 'Tautulli Remote App',
+ 'name': 'remoteapp',
+ 'id': AGENT_IDS['remoteapp'],
+ 'class': TAUTULLIREMOTEAPP,
'action_types': ('all',)
},
{'label': 'Boxcar',
@@ -882,188 +882,6 @@ class Notifier(object):
return config_options
-class ANDROIDAPP(Notifier):
- """
- Tautulli Remote Android app notifications
- """
- NAME = 'Tautulli Remote Android App'
- _DEFAULT_CONFIG = {'device_id': '',
- 'priority': 3,
- 'notification_type': 0
- }
-
- def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
- # Check mobile device is still registered
- device = mobile_app.get_mobile_devices(device_id=self.config['device_id'])
- if not device:
- logger.warn("Tautulli Notifiers :: Unable to send Android app notification: device not registered.")
- return
- else:
- device = device[0]
-
- pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
-
- plaintext_data = {'notification_id': notification_id,
- 'subject': subject,
- 'body': body,
- 'action': action,
- 'priority': self.config['priority'],
- 'notification_type': self.config['notification_type'],
- 'session_key': pretty_metadata.parameters.get('session_key', ''),
- 'session_id': pretty_metadata.parameters.get('session_id', ''),
- 'user_id': pretty_metadata.parameters.get('user_id', ''),
- 'rating_key': pretty_metadata.parameters.get('rating_key', ''),
- 'poster_thumb': pretty_metadata.parameters.get('poster_thumb', '')}
-
- #logger.debug("Plaintext data: {}".format(plaintext_data))
-
- if CRYPTODOME:
- # Key generation
- salt = get_random_bytes(16)
- passphrase = device['device_token']
- key_length = 32 # AES256
- iterations = 1000
- key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations,
- prf=lambda p, s: HMAC.new(p, s, SHA1).digest())
-
- #logger.debug("Encryption key (base64): {}".format(base64.b64encode(key)))
-
- # Encrypt using AES GCM
- nonce = get_random_bytes(16)
- cipher = AES.new(key, AES.MODE_GCM, nonce)
- encrypted_data, gcm_tag = cipher.encrypt_and_digest(json.dumps(plaintext_data).encode('utf-8'))
- encrypted_data += gcm_tag
-
- #logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data)))
- #logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag)))
- #logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
- #logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
-
- payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
- 'include_player_ids': [device['onesignal_id']],
- 'contents': {'en': 'Tautulli Notification'},
- 'data': {'encrypted': True,
- 'cipher_text': base64.b64encode(encrypted_data),
- 'nonce': base64.b64encode(nonce),
- 'salt': base64.b64encode(salt),
- 'server_id': plexpy.CONFIG.PMS_UUID}
- }
- else:
- logger.warn("Tautulli Notifiers :: PyCryptodome library is missing. "
- "Android app notifications will be sent unecrypted. "
- "Install the library to encrypt the notifications.")
-
- payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
- 'include_player_ids': [device['onesignal_id']],
- 'contents': {'en': 'Tautulli Notification'},
- 'data': {'encrypted': False,
- 'plain_text': plaintext_data,
- 'server_id': plexpy.CONFIG.PMS_UUID}
- }
-
- #logger.debug("OneSignal payload: {}".format(payload))
-
- headers = {'Content-Type': 'application/json'}
-
- return self.make_request("https://onesignal.com/api/v1/notifications", headers=headers, json=payload)
-
- def get_devices(self):
- db = database.MonitorDatabase()
-
- try:
- query = 'SELECT * FROM mobile_devices WHERE official = 1 ' \
- 'AND onesignal_id IS NOT NULL AND onesignal_id != ""'
- result = db.select(query=query)
- except Exception as e:
- logger.warn("Tautulli Notifiers :: Unable to retrieve Android app devices list: %s." % e)
- return {'': ''}
-
- devices = {}
- for device in result:
- if device['friendly_name']:
- devices[device['device_id']] = device['friendly_name']
- else:
- devices[device['device_id']] = device['device_name']
-
- return devices
-
- def _return_config_options(self):
- config_option = []
-
- if not CRYPTODOME:
- config_option.append({
- 'label': 'Warning',
- 'description': 'The PyCryptodome library is missing. '
- 'The content of your notifications will be sent unencrypted! '
- 'Please install the library to encrypt the notification contents. '
- 'Instructions can be found in the '
- 'FAQ.' ,
- 'input_type': 'help'
- })
- else:
- config_option.append({
- 'label': 'Note',
- 'description': 'The PyCryptodome library was found. '
- 'The content of your notifications will be sent encrypted!',
- 'input_type': 'help'
- })
-
- config_option[-1]['description'] += '
Notifications are sent using the ' \
- '' \
- 'OneSignal. Some user data is collected and cannot be encrypted. ' \
- 'Please read the ' \
- 'OneSignal Privacy Policy for more details.'
-
- devices = self.get_devices()
-
- if not devices:
- config_option.append({
- 'label': 'Device',
- 'description': 'No mobile devices registered with OneSignal. '
- ''
- 'Get the Android App and register a device. '
- 'Note: Only devices registered with a valid OneSignal ID will appear in the list.',
- 'input_type': 'help'
- })
- else:
- config_option.append({
- 'label': 'Device',
- 'value': self.config['device_id'],
- 'name': 'androidapp_device_id',
- 'description': 'Select your mobile device or '
- ''
- 'register a new device with Tautulli. '
- 'Note: Only devices registered with a valid OneSignal ID will appear in the list.',
- 'input_type': 'select',
- 'select_options': devices
- })
-
- config_option.append({
- 'label': 'Priority',
- 'value': self.config['priority'],
- 'name': 'androidapp_priority',
- 'description': 'Set the notification priority.',
- 'input_type': 'select',
- 'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'}
- })
- config_option.append({
- 'label': 'Notification Image Type',
- 'value': self.config['notification_type'],
- 'name': 'androidapp_notification_type',
- 'description': 'Set the notification image type.',
- 'input_type': 'select',
- 'select_options': {0: 'No notification image',
- 1: 'Small image (Expandable text)',
- 2: 'Large image (Non-expandable text)'
- }
- })
-
- return config_option
-
-
class BOXCAR(Notifier):
"""
Boxcar notifications
@@ -3399,6 +3217,188 @@ class SLACK(Notifier):
return config_option
+class TAUTULLIREMOTEAPP(Notifier):
+ """
+ Tautulli Remote app notifications
+ """
+ NAME = 'Tautulli Remote App'
+ _DEFAULT_CONFIG = {'device_id': '',
+ 'priority': 3,
+ 'notification_type': 0
+ }
+
+ def agent_notify(self, subject='', body='', action='', notification_id=None, **kwargs):
+ # Check mobile device is still registered
+ device = mobile_app.get_mobile_devices(device_id=self.config['device_id'])
+ if not device:
+ logger.warn("Tautulli Notifiers :: Unable to send Tautulli Remote app notification: device not registered.")
+ return
+ else:
+ device = device[0]
+
+ pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
+
+ plaintext_data = {'notification_id': notification_id,
+ 'subject': subject,
+ 'body': body,
+ 'action': action,
+ 'priority': self.config['priority'],
+ 'notification_type': self.config['notification_type'],
+ 'session_key': pretty_metadata.parameters.get('session_key', ''),
+ 'session_id': pretty_metadata.parameters.get('session_id', ''),
+ 'user_id': pretty_metadata.parameters.get('user_id', ''),
+ 'rating_key': pretty_metadata.parameters.get('rating_key', ''),
+ 'poster_thumb': pretty_metadata.parameters.get('poster_thumb', '')}
+
+ #logger.debug("Plaintext data: {}".format(plaintext_data))
+
+ if CRYPTODOME:
+ # Key generation
+ salt = get_random_bytes(16)
+ passphrase = device['device_token']
+ key_length = 32 # AES256
+ iterations = 1000
+ key = PBKDF2(passphrase, salt, dkLen=key_length, count=iterations,
+ prf=lambda p, s: HMAC.new(p, s, SHA1).digest())
+
+ #logger.debug("Encryption key (base64): {}".format(base64.b64encode(key)))
+
+ # Encrypt using AES GCM
+ nonce = get_random_bytes(16)
+ cipher = AES.new(key, AES.MODE_GCM, nonce)
+ encrypted_data, gcm_tag = cipher.encrypt_and_digest(json.dumps(plaintext_data).encode('utf-8'))
+ encrypted_data += gcm_tag
+
+ #logger.debug("Encrypted data (base64): {}".format(base64.b64encode(encrypted_data)))
+ #logger.debug("GCM tag (base64): {}".format(base64.b64encode(gcm_tag)))
+ #logger.debug("Nonce (base64): {}".format(base64.b64encode(nonce)))
+ #logger.debug("Salt (base64): {}".format(base64.b64encode(salt)))
+
+ payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
+ 'include_player_ids': [device['onesignal_id']],
+ 'contents': {'en': 'Tautulli Notification'},
+ 'data': {'encrypted': True,
+ 'cipher_text': base64.b64encode(encrypted_data),
+ 'nonce': base64.b64encode(nonce),
+ 'salt': base64.b64encode(salt),
+ 'server_id': plexpy.CONFIG.PMS_UUID}
+ }
+ else:
+ logger.warn("Tautulli Notifiers :: PyCryptodome library is missing. "
+ "Tautulli Remote app notifications will be sent unecrypted. "
+ "Install the library to encrypt the notifications.")
+
+ payload = {'app_id': mobile_app._ONESIGNAL_APP_ID,
+ 'include_player_ids': [device['onesignal_id']],
+ 'contents': {'en': 'Tautulli Notification'},
+ 'data': {'encrypted': False,
+ 'plain_text': plaintext_data,
+ 'server_id': plexpy.CONFIG.PMS_UUID}
+ }
+
+ #logger.debug("OneSignal payload: {}".format(payload))
+
+ headers = {'Content-Type': 'application/json'}
+
+ return self.make_request("https://onesignal.com/api/v1/notifications", headers=headers, json=payload)
+
+ def get_devices(self):
+ db = database.MonitorDatabase()
+
+ try:
+ query = 'SELECT * FROM mobile_devices WHERE official = 1 ' \
+ 'AND onesignal_id IS NOT NULL AND onesignal_id != ""'
+ result = db.select(query=query)
+ except Exception as e:
+ logger.warn("Tautulli Notifiers :: Unable to retrieve Tautulli Remote app devices list: %s." % e)
+ return {'': ''}
+
+ devices = {}
+ for device in result:
+ if device['friendly_name']:
+ devices[device['device_id']] = device['friendly_name']
+ else:
+ devices[device['device_id']] = device['device_name']
+
+ return devices
+
+ def _return_config_options(self):
+ config_option = []
+
+ if not CRYPTODOME:
+ config_option.append({
+ 'label': 'Warning',
+ 'description': 'The PyCryptodome library is missing. '
+ 'The content of your notifications will be sent unencrypted! '
+ 'Please install the library to encrypt the notification contents. '
+ 'Instructions can be found in the '
+ 'FAQ.' ,
+ 'input_type': 'help'
+ })
+ else:
+ config_option.append({
+ 'label': 'Note',
+ 'description': 'The PyCryptodome library was found. '
+ 'The content of your notifications will be sent encrypted!',
+ 'input_type': 'help'
+ })
+
+ config_option[-1]['description'] += '
Notifications are sent using the ' \
+ '' \
+ 'OneSignal. Some user data is collected and cannot be encrypted. ' \
+ 'Please read the ' \
+ 'OneSignal Privacy Policy for more details.'
+
+ devices = self.get_devices()
+
+ if not devices:
+ config_option.append({
+ 'label': 'Device',
+ 'description': 'No mobile devices registered with OneSignal. '
+ ''
+ 'Get the Tautulli Remote App and register a device. '
+ 'Note: Only devices registered with a valid OneSignal ID will appear in the list.',
+ 'input_type': 'help'
+ })
+ else:
+ config_option.append({
+ 'label': 'Device',
+ 'value': self.config['device_id'],
+ 'name': 'remoteapp_device_id',
+ 'description': 'Select your mobile device or '
+ ''
+ 'register a new device with Tautulli. '
+ 'Note: Only devices registered with a valid OneSignal ID will appear in the list.',
+ 'input_type': 'select',
+ 'select_options': devices
+ })
+
+ config_option.append({
+ 'label': 'Priority',
+ 'value': self.config['priority'],
+ 'name': 'remoteapp_priority',
+ 'description': 'Set the notification priority.',
+ 'input_type': 'select',
+ 'select_options': {1: 'Minimum', 2: 'Low', 3: 'Normal', 4: 'High'}
+ })
+ config_option.append({
+ 'label': 'Notification Image Type',
+ 'value': self.config['notification_type'],
+ 'name': 'remoteapp_notification_type',
+ 'description': 'Set the notification image type.',
+ 'input_type': 'select',
+ 'select_options': {0: 'No notification image',
+ 1: 'Small image (Expandable text)',
+ 2: 'Large image (Non-expandable text)'
+ }
+ })
+
+ return config_option
+
+
class TELEGRAM(Notifier):
"""
Telegram notifications