diff --git a/data/interfaces/default/notification_config.html b/data/interfaces/default/notification_config.html index 8732e6a9..c61740fb 100644 --- a/data/interfaces/default/notification_config.html +++ b/data/interfaces/default/notification_config.html @@ -48,7 +48,7 @@ from plexpy import helpers
-
+

Set a custom body.

+ %if agent['name'] == 'Scripts': +
+ + +

Pick your script

+
+
+ + +

Pick your action

+ +
+
+ + +

Set a custom script args: -zomg --x.

+
+ % endif
@@ -148,7 +183,10 @@ from plexpy import helpers url: 'test_notifier', data: { config_id: '${agent["id"]}', subject: $('#test_subject').val(), - body: $('#test_body').val() }, + body: $('#test_body').val(), + notify_action: $('#test_script_action').val(), + script: $('#test_script').val(), + script_args: $('#test_script_args').val() }, cache: false, async: true, complete: function (xhr, status) { @@ -164,6 +202,12 @@ from plexpy import helpers return false; }); + $('#scripts_folder').on('change', function () { + doAjaxCall('set_notification_config', $(this), 'tabs', true); + reloadModal(); + return false; + }); + // Never send checkbox values directly, always substitute value in hidden input. $('.checkboxes').click(function () { var configToggle = $(this).data('id'); diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 2251df3b..2bf3b653 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -751,7 +751,22 @@ available_notification_agents = sorted(notifiers.available_notification_agents() +
    +
  • + + +
  • +

@@ -1111,6 +1126,14 @@ available_notification_agents = sorted(notifiers.available_notification_agents() {transcode_audio_channels} The audio channels of the transcoded media. + + {streams} + The number of concurrent streams. + + + {action} + The action that trigged the notification. + @@ -1575,6 +1598,7 @@ $(document).ready(function() { var accordion_session = new Accordion($('#accordion-session'), false); var accordion_timeline = new Accordion($('#accordion-timeline'), false); + var accordion_scripts = new Accordion($('#accordion-scripts'), false); var cards = "${config['home_stats_cards']}".split(/[\s,]+/); cards.forEach(function (item) { diff --git a/plexpy/config.py b/plexpy/config.py index a7d4f3b3..d899f583 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -1,6 +1,4 @@ import plexpy.logger -import itertools -import os import re from configobj import ConfigObj @@ -14,8 +12,6 @@ def bool_int(value): value = 0 return int(bool(value)) - - _CONFIG_DEFINITIONS = { 'DATE_FORMAT': (str, 'General', 'YYYY-MM-DD'), 'GROUPING_GLOBAL_HISTORY': (int, 'PlexWatch', 0), @@ -194,6 +190,7 @@ _CONFIG_DEFINITIONS = { 'NOTIFY_ON_EXTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is back up.'), 'NOTIFY_ON_INTUP_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'), 'NOTIFY_ON_INTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is back up.'), + 'NOTIFY_SCRIPTS_ARGS_TEXT': (unicode, 'Monitoring', ''), 'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'), 'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0), @@ -298,6 +295,30 @@ _CONFIG_DEFINITIONS = { 'SLACK_ON_INTDOWN': (int, 'Slack', 0), 'SLACK_ON_EXTUP': (int, 'Slack', 0), 'SLACK_ON_INTUP': (int, 'Slack', 0), + 'SCRIPTS_ENABLED': (int, 'Scripts', 0), + 'SCRIPTS_FOLDER': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_PLAY': (int, 'Scripts', 0), + 'SCRIPTS_ON_STOP': (int, 'Scripts', 0), + 'SCRIPTS_ON_PAUSE': (int, 'Scripts', 0), + 'SCRIPTS_ON_RESUME': (int, 'Scripts', 0), + 'SCRIPTS_ON_BUFFER': (int, 'Scripts', 0), + 'SCRIPTS_ON_WATCHED': (int, 'Scripts', 0), + 'SCRIPTS_ON_CREATED': (int, 'Scripts', 0), + 'SCRIPTS_ON_EXTDOWN': (int, 'Scripts', 0), + 'SCRIPTS_ON_EXTUP': (int, 'Scripts', 0), + 'SCRIPTS_ON_INTDOWN': (int, 'Scripts', 0), + 'SCRIPTS_ON_INTUP': (int, 'Scripts', 0), + 'SCRIPTS_ON_PLAY_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_STOP_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_PAUSE_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_RESUME_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_BUFFER_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_WATCHED_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_CREATED_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_EXTDOWN_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_EXTUP_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_INTDOWN_SCRIPT': (unicode, 'Scripts', ''), + 'SCRIPTS_ON_INTUP_SCRIPT': (unicode, 'Scripts', ''), 'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''), 'TELEGRAM_ENABLED': (int, 'Telegram', 0), 'TELEGRAM_CHAT_ID': (str, 'Telegram', ''), @@ -351,6 +372,8 @@ _CONFIG_DEFINITIONS = { 'XBMC_ON_EXTUP': (int, 'XBMC', 0), 'XBMC_ON_INTUP': (int, 'XBMC', 0) } + + # pylint:disable=R0902 # it might be nice to refactor for fewer instance variables class Config(object): diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index f5b907ec..f642fac6 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -13,15 +13,17 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect -import plexpy +import re import time +from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect +import plexpy + def notify(stream_data=None, notify_action=None): from plexpy import users - + if stream_data and notify_action: # Check if notifications enabled for user user_data = users.Users() @@ -41,7 +43,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -51,7 +56,9 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -61,7 +68,9 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -71,7 +80,9 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -80,7 +91,9 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -94,7 +107,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -106,7 +122,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -118,7 +137,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -127,7 +149,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -136,7 +161,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -145,7 +173,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -154,7 +185,10 @@ def notify(stream_data=None, notify_action=None): notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) + # Set the notification state in the db set_notify_state(session=stream_data, state=notify_action, agent_info=agent) @@ -181,7 +215,9 @@ def notify_timeline(timeline_data=None, notify_action=None): notify_strings = build_notify_text(timeline=timeline_data, state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) # Set the notification state in the db set_notify_state(session=timeline_data, state=notify_action, agent_info=agent) @@ -192,25 +228,33 @@ def notify_timeline(timeline_data=None, notify_action=None): notify_strings = build_server_notify_text(state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) if agent['on_intdown'] and notify_action == 'intdown': # Build and send notification notify_strings = build_server_notify_text(state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) if agent['on_extup'] and notify_action == 'extup': # Build and send notification notify_strings = build_server_notify_text(state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) if agent['on_intup'] and notify_action == 'intup': # Build and send notification notify_strings = build_server_notify_text(state=notify_action) notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0], - body=notify_strings[1]) + body=notify_strings[1], + notify_action=notify_action, + script_args=notify_strings[2]) else: logger.debug(u"PlexPy Notifier :: Notify timeline called but incomplete data received.") @@ -237,6 +281,7 @@ def get_notify_state(session): return notify_states + def get_notify_state_timeline(timeline): monitor_db = database.MonitorDatabase() result = monitor_db.select('SELECT on_created, agent_id ' @@ -293,7 +338,6 @@ def set_notify_state(session, state, agent_info): def build_notify_text(session=None, timeline=None, state=None): - import re # Get the server name server_name = plexpy.CONFIG.PMS_NAME @@ -318,6 +362,8 @@ def build_notify_text(session=None, timeline=None, state=None): pms_connect = pmsconnect.PmsConnect() metadata_list = pms_connect.get_metadata_details(rating_key=rating_key) + stream_count = pms_connect.get_current_activity().get('stream_count', '') + if metadata_list: metadata = metadata_list['metadata'] else: @@ -327,13 +373,13 @@ def build_notify_text(session=None, timeline=None, state=None): # Check for exclusion tags if metadata['media_type'] == 'movie': # Regex pattern to remove the text in the tags we don't want - pattern = re.compile('\n*[^>]+.\n*|\n*[^>]+.\n*', re.IGNORECASE|re.DOTALL) + pattern = re.compile('\n*[^>]+.\n*|\n*[^>]+.\n*', re.IGNORECASE | re.DOTALL) elif metadata['media_type'] == 'show' or metadata['media_type'] == 'episode': # Regex pattern to remove the text in the tags we don't want - pattern = re.compile('\n*[^>]+.\n*|\n*?[^>]+.\n*', re.IGNORECASE|re.DOTALL) + pattern = re.compile('\n*[^>]+.\n*|\n*?[^>]+.\n*', re.IGNORECASE | re.DOTALL) elif metadata['media_type'] == 'artist' or metadata['media_type'] == 'track': # Regex pattern to remove the text in the tags we don't want - pattern = re.compile('\n*[^>]+.\n*|\n*[^>]+.\n*', re.IGNORECASE|re.DOTALL) + pattern = re.compile('\n*[^>]+.\n*|\n*[^>]+.\n*', re.IGNORECASE | re.DOTALL) else: pattern = None @@ -356,6 +402,7 @@ def build_notify_text(session=None, timeline=None, state=None): on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT)) on_created_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT)) on_created_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT)) + script_args_text = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT)) else: on_start_subject = plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT @@ -371,6 +418,7 @@ def build_notify_text(session=None, timeline=None, state=None): on_watched_body = plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT on_created_subject = plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT on_created_body = plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT + script_args_text = plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT # Create a title if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track': @@ -465,7 +513,7 @@ def build_notify_text(session=None, timeline=None, state=None): artist_name = metadata['grandparent_title'] album_name = metadata['parent_title'] track_name = metadata['title'] - + available_params = {'server_name': server_name, 'server_uptime': server_uptime, 'user': user, @@ -516,12 +564,29 @@ def build_notify_text(session=None, timeline=None, state=None): 'summary': metadata['summary'], 'tagline': metadata['tagline'], 'rating': metadata['rating'], - 'duration': duration + 'duration': duration, + 'action': state, + 'streams': stream_count } # Default subject text subject_text = 'PlexPy (%s)' % server_name + # Default scripts args + script_args = [] + + # Regex to match {param} but not "{param}" + params_to_quote = re.compile(r'(?"', script_args_text) + + if script_args_text: + try: + script_args = [unicode(arg).format(**available_params) for arg in script_args_text.split()] + except LookupError as e: + logger.error(u"PlexPy Notifier :: Unable to parse field %s in script argument. Using fallback." % e) + except Exception as e: + logger.error(u"PlexPy Notifier :: Unable to parse custom script arguments %s. Using fallback." % e) + if state == 'play': # Default body text body_text = '%s (%s) is watching %s' % (session['friendly_name'], @@ -543,9 +608,9 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'stop': # Default body text body_text = '%s (%s) has stopped %s' % (session['friendly_name'], @@ -567,9 +632,9 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'pause': # Default body text body_text = '%s (%s) has paused %s' % (session['friendly_name'], @@ -591,9 +656,9 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'resume': # Default body text body_text = '%s (%s) has resumed %s' % (session['friendly_name'], @@ -615,9 +680,9 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'buffer': # Default body text body_text = '%s (%s) is buffering %s' % (session['friendly_name'], @@ -639,9 +704,9 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'watched': # Default body text body_text = '%s (%s) has watched %s' % (session['friendly_name'], @@ -663,9 +728,9 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'created': # Default body text body_text = '%s was recently added to Plex.' % full_title @@ -685,12 +750,13 @@ def build_notify_text(session=None, timeline=None, state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: return None + def build_server_notify_text(state=None): # Get the server name server_name = plexpy.CONFIG.PMS_NAME @@ -716,11 +782,28 @@ def build_server_notify_text(state=None): on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT available_params = {'server_name': server_name, - 'server_uptime': server_uptime} + 'server_uptime': server_uptime, + 'action': state} # Default text subject_text = 'PlexPy (%s)' % server_name + # Default scripts args + script_args = [] + script_args_text = plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT + + # Regex to match {param} but not "{param}" + params_to_quote = re.compile(r'(?"', script_args_text) + + if script_args_text: + try: + script_args = [unicode(arg).format(**available_params) for arg in script_args_text.split()] + except LookupError as e: + logger.error(u"PlexPy Notifier :: Unable to parse field %s in script argument. Using fallback." % e) + except Exception as e: + logger.error(u"PlexPy Notifier :: Unable to parse custom script arguments %s. Using fallback." % e) + if state == 'extdown': # Default body text body_text = 'The Plex Media Server remote access is down.' @@ -740,9 +823,10 @@ def build_server_notify_text(state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] + elif state == 'intdown': # Default body text body_text = 'The Plex Media Server is down.' @@ -762,9 +846,9 @@ def build_server_notify_text(state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] if state == 'extup': # Default body text body_text = 'The Plex Media Server remote access is back up.' @@ -784,9 +868,9 @@ def build_server_notify_text(state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] elif state == 'intup': # Default body text body_text = 'The Plex Media Server is back up.' @@ -806,14 +890,16 @@ def build_server_notify_text(state=None): except: logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") - return [subject_text, body_text] + return [subject_text, body_text, script_args] else: - return [subject_text, body_text] + return [subject_text, body_text, script_args] + else: return None + def strip_tag(data): import re p = re.compile(r'<.*?>') - return p.sub('', data) \ No newline at end of file + return p.sub('', data) diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index 16100738..b00c75a7 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -13,32 +13,31 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, helpers, common, request -from plexpy.helpers import checked, radio - -from xml.dom import minidom -from httplib import HTTPSConnection -from urlparse import parse_qsl from urlparse import urlparse -from urllib import urlencode -from pynma import pynma - import base64 +import json import cherrypy +from email.mime.text import MIMEText +import email.utils +from httplib import HTTPSConnection +import os +import shlex +import smtplib +import subprocess + +from urllib import urlencode import urllib import urllib2 -import plexpy -import os.path -import subprocess -import gntp.notifier -import json +from urlparse import parse_qsl +from pynma import pynma +import gntp.notifier import oauth2 as oauth import pythontwitter as twitter -from email.mime.text import MIMEText -import smtplib -import email.utils +import plexpy +from plexpy import logger, helpers, request +from plexpy.helpers import checked AGENT_IDS = {"Growl": 0, "Prowl": 1, @@ -54,7 +53,9 @@ AGENT_IDS = {"Growl": 0, "Twitter": 11, "IFTTT": 12, "Telegram": 13, - "Slack":14} + "Slack": 14, + "Scripts": 15} + def available_notification_agents(): agents = [{'name': 'Growl', @@ -294,7 +295,25 @@ def available_notification_agents(): 'on_intdown': plexpy.CONFIG.SLACK_ON_INTDOWN, 'on_extup': plexpy.CONFIG.SLACK_ON_EXTUP, 'on_intup': plexpy.CONFIG.SLACK_ON_INTUP - } + }, + {'name': 'Scripts', + 'id': AGENT_IDS['Scripts'], + 'config_prefix': 'scripts', + 'has_config': True, + 'state': checked(plexpy.CONFIG.SCRIPTS_ENABLED), + 'on_play': plexpy.CONFIG.SCRIPTS_ON_PLAY, + 'on_stop': plexpy.CONFIG.SCRIPTS_ON_STOP, + 'on_pause': plexpy.CONFIG.SCRIPTS_ON_PAUSE, + 'on_resume': plexpy.CONFIG.SCRIPTS_ON_RESUME, + 'on_buffer': plexpy.CONFIG.SCRIPTS_ON_BUFFER, + 'on_watched': plexpy.CONFIG.SCRIPTS_ON_WATCHED, + 'on_created': plexpy.CONFIG.SCRIPTS_ON_CREATED, + 'on_extdown': plexpy.CONFIG.SCRIPTS_ON_EXTDOWN, + 'on_extup': plexpy.CONFIG.SCRIPTS_ON_EXTUP, + 'on_intdown': plexpy.CONFIG.SCRIPTS_ON_INTDOWN, + 'on_intup': plexpy.CONFIG.SCRIPTS_ON_INTUP + } + ] # OSX Notifications should only be visible if it can be used @@ -320,6 +339,7 @@ def available_notification_agents(): return agents + def get_notification_agent_config(config_id): if config_id: config_id = int(config_id) @@ -364,17 +384,21 @@ def get_notification_agent_config(config_id): iftttClient = IFTTT() return iftttClient.return_config_options() elif config_id == 13: - telegramClient = TELEGRAM() - return telegramClient.return_config_options() + telegramClient = TELEGRAM() + return telegramClient.return_config_options() elif config_id == 14: slackClient = SLACK() return slackClient.return_config_options() + elif config_id == 15: + script = Scripts() + return script.return_config_options() else: return [] else: return [] -def send_notification(config_id, subject, body): + +def send_notification(config_id, subject, body, **kwargs): if str(config_id).isdigit(): config_id = int(config_id) @@ -418,11 +442,14 @@ def send_notification(config_id, subject, body): iftttClient = IFTTT() iftttClient.notify(subject=subject, message=body) elif config_id == 13: - telegramClient = TELEGRAM() - telegramClient.notify(message=body, event=subject) + telegramClient = TELEGRAM() + telegramClient.notify(message=body, event=subject) elif config_id == 14: slackClient = SLACK() slackClient.notify(message=body, event=subject) + elif config_id == 15: + scripts = Scripts() + scripts.notify(message=body, subject=subject, **kwargs) else: logger.debug(u"PlexPy Notifier :: Unknown agent id received.") else: @@ -503,7 +530,7 @@ class GROWL(object): logger.info(u"Growl notifications sent.") def updateLibrary(self): - #For uniformity reasons not removed + # For uniformity reasons not removed return def test(self, host, password): @@ -530,6 +557,7 @@ class GROWL(object): return config_option + class PROWL(object): """ Prowl notifications. @@ -556,9 +584,10 @@ class PROWL(object): 'priority': plexpy.CONFIG.PROWL_PRIORITY} http_handler.request("POST", - "/publicapi/add", - headers={'Content-type': "application/x-www-form-urlencoded"}, - body=urlencode(data)) + "/publicapi/add", + headers={'Content-type': "application/x-www-form-urlencoded"}, + body=urlencode(data)) + response = http_handler.getresponse() request_status = response.status @@ -573,7 +602,7 @@ class PROWL(object): return False def updateLibrary(self): - #For uniformity reasons not removed + # For uniformity reasons not removed return def test(self, keys, priority): @@ -601,6 +630,7 @@ class PROWL(object): return config_option + class XBMC(object): """ XBMC notifications @@ -640,19 +670,19 @@ class XBMC(object): header = subject message = message - time = "3000" # in ms + time = "3000" # in ms for host in hosts: logger.info('Sending notification command to XMBC @ ' + host) try: version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major'] - if version < 12: #Eden + if version < 12: # Eden notification = header + "," + message + "," + time notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'} request = self._sendhttp(host, notifycommand) - else: #Frodo + else: # Frodo params = {'title': header, 'message': message, 'displaytime': int(time)} request = self._sendjson(host, 'GUI.ShowNotification', params) @@ -685,6 +715,7 @@ class XBMC(object): return config_option + class Plex(object): def __init__(self): @@ -725,7 +756,7 @@ class Plex(object): header = subject message = message - time = "3000" # in ms + time = "3000" # in ms for host in hosts: logger.info('Sending notification command to Plex Media Server @ ' + host) @@ -763,6 +794,7 @@ class Plex(object): return config_option + class NMA(object): def __init__(self): @@ -821,6 +853,7 @@ class NMA(object): return config_option + class PUSHBULLET(object): def __init__(self): @@ -848,10 +881,11 @@ class PUSHBULLET(object): data['channel_tag'] = self.channel_tag http_handler.request("POST", - "/v2/pushes", - headers={'Content-type': "application/json", - 'Authorization': 'Basic %s' % base64.b64encode(plexpy.CONFIG.PUSHBULLET_APIKEY + ":")}, - body=json.dumps(data)) + "/v2/pushes", + headers={'Content-type': "application/json", + 'Authorization': 'Basic %s' % base64.b64encode(plexpy.CONFIG.PUSHBULLET_APIKEY + ":")}, + body=json.dumps(data)) + response = http_handler.getresponse() request_status = response.status # logger.debug(u"PushBullet response status: %r" % request_status) @@ -880,8 +914,9 @@ class PUSHBULLET(object): if plexpy.CONFIG.PUSHBULLET_APIKEY: http_handler = HTTPSConnection("api.pushbullet.com") http_handler.request("GET", "/v2/devices", - headers={'Content-type': "application/json", - 'Authorization': 'Basic %s' % base64.b64encode(plexpy.CONFIG.PUSHBULLET_APIKEY + ":")}) + headers={'Content-type': "application/json", + 'Authorization': 'Basic %s' % base64.b64encode(plexpy.CONFIG.PUSHBULLET_APIKEY + ":")}) + response = http_handler.getresponse() request_status = response.status @@ -926,6 +961,7 @@ class PUSHBULLET(object): return config_option + class PUSHALOT(object): def __init__(self): @@ -937,9 +973,9 @@ class PUSHALOT(object): pushalot_authorizationtoken = plexpy.CONFIG.PUSHALOT_APIKEY - #logger.debug(u"Pushalot event: " + event) - #logger.debug(u"Pushalot message: " + message) - #logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) + # logger.debug(u"Pushalot event: " + event) + # logger.debug(u"Pushalot message: " + message) + # logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) http_handler = HTTPSConnection("pushalot.com") @@ -948,15 +984,15 @@ class PUSHALOT(object): 'Body': message.encode("utf-8")} http_handler.request("POST", - "/api/sendmessage", - headers={'Content-type': "application/x-www-form-urlencoded"}, - body=urlencode(data)) + "/api/sendmessage", + headers={'Content-type': "application/x-www-form-urlencoded"}, + body=urlencode(data)) response = http_handler.getresponse() request_status = response.status - #logger.debug(u"Pushalot response status: %r" % request_status) - #logger.debug(u"Pushalot response headers: %r" % response.getheaders()) - #logger.debug(u"Pushalot response body: %r" % response.read()) + # logger.debug(u"Pushalot response status: %r" % request_status) + # logger.debug(u"Pushalot response headers: %r" % response.getheaders()) + # logger.debug(u"Pushalot response body: %r" % response.read()) if request_status == 200: logger.info(u"Pushalot notifications sent.") @@ -979,6 +1015,7 @@ class PUSHALOT(object): return config_option + class PUSHOVER(object): def __init__(self): @@ -1025,7 +1062,7 @@ class PUSHOVER(object): return False def updateLibrary(self): - #For uniformity reasons not removed + # For uniformity reasons not removed return def test(self, keys, priority, sound): @@ -1042,7 +1079,7 @@ class PUSHOVER(object): http_handler.request("GET", "/1/sounds.json?token=" + self.application_token) response = http_handler.getresponse() request_status = response.status - + if request_status == 200: data = json.loads(response.read()) sounds = data.get('sounds', {}) @@ -1054,7 +1091,7 @@ class PUSHOVER(object): else: logger.info(u"Unable to retrieve Pushover notification sounds list.") return {'': ''} - + else: return {'': ''} @@ -1089,6 +1126,7 @@ class PUSHOVER(object): return config_option + class TwitterNotifier(object): REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' @@ -1186,24 +1224,25 @@ class TwitterNotifier(object): 'description': 'Step 1: Click Request button above. (Ensure you allow the browser pop-up).', 'input_type': 'button' }, - {'label': 'Authorisation Key', - 'value': '', - 'name': 'twitter_key', - 'description': 'Step 2: Input the authorisation key you received from Step 1.', - 'input_type': 'text' + {'label': 'Authorisation Key', + 'value': '', + 'name': 'twitter_key', + 'description': 'Step 2: Input the authorisation key you received from Step 1.', + 'input_type': 'text' }, - {'label': 'Verify Key', - 'value': 'Verify Key', - 'name': 'twitterStep2', - 'description': 'Step 3: Verify the key.', - 'input_type': 'button' + {'label': 'Verify Key', + 'value': 'Verify Key', + 'name': 'twitterStep2', + 'description': 'Step 3: Verify the key.', + 'input_type': 'button' }, - {'input_type': 'nosave' + {'input_type': 'nosave' } ] return config_option + class OSX_NOTIFY(object): def __init__(self): @@ -1211,7 +1250,7 @@ class OSX_NOTIFY(object): self.objc = __import__("objc") self.AppKit = __import__("AppKit") except: - #logger.error(u"PlexPy Notifier :: Cannot load OSX Notifications agent.") + # logger.error(u"PlexPy Notifier :: Cannot load OSX Notifications agent.") pass def validate(self): @@ -1228,7 +1267,7 @@ class OSX_NOTIFY(object): def wrapper(self, *args, **kwargs): return func(self, old_IMP, *args, **kwargs) new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector, - signature=old_IMP.signature) + signature=old_IMP.signature) self.objc.classAddMethod(cls, SEL, new_IMP) def notify(self, title, subtitle=None, text=None, sound=True, image=None): @@ -1258,7 +1297,7 @@ class OSX_NOTIFY(object): if image: source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image) notification.setContentImage_(source_img) - #notification.set_identityImage_(source_img) + # notification.set_identityImage_(source_img) notification.setHasActionButton_(False) notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() @@ -1287,6 +1326,7 @@ class OSX_NOTIFY(object): return config_option + class BOXCAR(object): def __init__(self): @@ -1347,7 +1387,7 @@ class BOXCAR(object): 'flourish': 'Flourish', 'harp': 'Harp', 'light': 'Light', - 'magic-chime':'Magic Chime', + 'magic-chime': 'Magic Chime', 'magic-coin': 'Magic Coin', 'no-sound': 'No Sound', 'notifier-1': 'Notifier (1)', @@ -1363,6 +1403,7 @@ class BOXCAR(object): return config_option + class Email(object): def __init__(self): @@ -1523,10 +1564,12 @@ class IFTTT(object): ' as value1 and value2 respectively.', 'input_type': 'text' } + ] return config_option + class TELEGRAM(object): def __init__(self): @@ -1565,7 +1608,7 @@ class TELEGRAM(object): return False def updateLibrary(self): - #For uniformity reasons not removed + # For uniformity reasons not removed return def test(self, bot_token, chat_id): @@ -1676,3 +1719,240 @@ class SLACK(object): ] return config_option + + +class Scripts(object): + + def __init__(self, **kwargs): + pass + + def conf(self, options): + return cherrypy.config['config'].get('Scripts', options) + + def updateLibrary(self): + # For uniformity reasons not removed + return + + def test(self, subject, message, *args, **kwargs): + self.notify(subject, message, *args, **kwargs) + return + + def list_scripts(self): + scriptdir = plexpy.CONFIG.SCRIPTS_FOLDER + scripts = {'': ''} + + if scriptdir and not os.path.exists(scriptdir): + os.makedirs(scriptdir) + + for root, dirs, files in os.walk(scriptdir): + for f in files: + name, ext = os.path.splitext(f) + if ext in ('.rb', '.pl', '.bat', '.py', '.sh', '.cmd', '.php'): + rfp = os.path.join(os.path.relpath(root, scriptdir), f) + fp = os.path.join(root, f) + scripts[fp] = rfp + + return scripts + + def notify(self, subject='', message='', notify_action='', script_args='', *args, **kwargs): + """ + Args: + subject(string, optional): Head text, + message(string, optional): Body text, + notify_action(string): 'play' + script_args(list): ["python2", '-p', '-zomg'] + """ + logger.debug(u'Trying to run notify script subject: %s message: %s, action: %s script_args: %s' % + (subject, message, notify_action, script_args)) + + prefix = '' + script = kwargs.get('script', '') # for manual scripts + + if not plexpy.CONFIG.SCRIPTS_FOLDER: + return + + # Make sure we use the correct script.. + if notify_action == 'play': + script = plexpy.CONFIG.SCRIPTS_ON_PLAY_SCRIPT + + elif notify_action == 'stop': + script = plexpy.CONFIG.SCRIPTS_ON_STOP_SCRIPT + + elif notify_action == 'pause': + script = plexpy.CONFIG.SCRIPTS_ON_PAUSE_SCRIPT + + elif notify_action == 'resume': + script = plexpy.CONFIG.SCRIPTS_ON_RESUME_SCRIPT + + elif notify_action == 'buffer': + script = plexpy.CONFIG.SCRIPTS_ON_BUFFER_SCRIPT + + elif notify_action == 'extdown': + script = plexpy.CONFIG.SCRIPTS_ON_EXTDOWN_SCRIPT + + elif notify_action == 'extup': + script = plexpy.CONFIG.SCRIPTS_ON_EXTUP_SCRIPT + + elif notify_action == 'intdown': + script = plexpy.CONFIG.SCRIPTS_ON_INTDOWN_SCRIPT + + elif notify_action == 'intup': + script = plexpy.CONFIG.SCRIPTS_ON_INTUP_SCRIPT + + elif notify_action == 'created': + script = plexpy.CONFIG.SCRIPTS_ON_CREATED_SCRIPT + + elif notify_action == 'watched': + script = plexpy.CONFIG.SCRIPTS_ON_WATCHED_SCRIPT + + # Dont try to run the script + # if the action does not have one + if not script: + logger.debug(u'%s has no script, exiting..' % notify_action) + return + + name, ext = os.path.splitext(script) + + if ext == '.py': + prefix = 'python' + elif ext == '.php': + prefix = 'php' + elif ext == '.pl': + prefix = 'perl' + elif ext == '.rb': + prefix = 'ruby' + + if os.name == 'nt': + script = script.encode(plexpy.SYS_ENCODING, 'ignore') + script = [script] + + if prefix: + script.insert(0, prefix) + + # for manual notifications + if script_args and isinstance(script_args, basestring): + # attemps for format it for the user + script_args = shlex.split(script_args) + + # Windows handles unicode very badly. + # https://bugs.python.org/issue19264 + if script_args and os.name == 'nt': + script_args = [s.encode(plexpy.SYS_ENCODING, 'ignore') for s in script_args] + + # Allow overrides for shitty systems + if prefix and script_args: + if script_args[0] in ['python2', 'python', 'php', 'ruby', 'perl']: + script[0] = script_args[0] + del script_args[0] + + script.extend(script_args) + + logger.debug(u'Full script is %s' % script) + + try: + p = subprocess.Popen(script, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=plexpy.CONFIG.SCRIPTS_FOLDER) + + out, error = p.communicate() + status = p.returncode + + if out and status: + out = out.strip() + logger.debug(u'%s returned %s' % (script, out)) + + if error: + error = error.strip() + logger.error(u'%s' % error) + + except OSError as out: + logger.error(u'Failed to run %s error %s' % (script, out)) + + def return_config_options(self): + config_option = [{'label': 'Script folder', + 'value': plexpy.CONFIG.SCRIPTS_FOLDER, + 'name': 'scripts_folder', + 'description': 'Add your script folder.', + 'input_type': 'text', + }, + {'label': 'Playback Start', + 'value': plexpy.CONFIG.SCRIPTS_ON_PLAY_SCRIPT, + 'name': 'scripts_on_play_script', + 'description': 'Pick the script for on play.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Playback Stop', + 'value': plexpy.CONFIG.SCRIPTS_ON_STOP_SCRIPT, + 'name': 'scripts_on_stop_script', + 'description': 'Pick the script for on stop.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Playback Pause', + 'value': plexpy.CONFIG.SCRIPTS_ON_PAUSE_SCRIPT, + 'name': 'scripts_on_pause_script', + 'description': 'Pick the script for on pause.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Playback Resume', + 'value': plexpy.CONFIG.SCRIPTS_ON_RESUME_SCRIPT, + 'name': 'scripts_on_resume_script', + 'description': 'Pick the script for on resume.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Watched', + 'value': plexpy.CONFIG.SCRIPTS_ON_WATCHED_SCRIPT, + 'name': 'scripts_on_watched_script', + 'description': 'Pick the script for on watched.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Buffer Warnings', + 'value': plexpy.CONFIG.SCRIPTS_ON_BUFFER_SCRIPT, + 'name': 'scripts_on_buffer_script', + 'description': 'Pick the script for buffer warnings.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Recently Added', + 'value': plexpy.CONFIG.SCRIPTS_ON_CREATED_SCRIPT, + 'name': 'scripts_on_created_script', + 'description': 'Pick the script for recently added.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Plex Remote Access Down', + 'value': plexpy.CONFIG.SCRIPTS_ON_EXTDOWN_SCRIPT, + 'name': 'scripts_on_extdown_script', + 'description': 'Pick the script for external connection down.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Plex Remote Access Up', + 'value': plexpy.CONFIG.SCRIPTS_ON_EXTUP_SCRIPT, + 'name': 'scripts_on_extup_script', + 'description': 'Pick the script for external connection up.', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Plex Server Down', + 'value': plexpy.CONFIG.SCRIPTS_ON_INTDOWN_SCRIPT, + 'name': 'scripts_on_intdown_script', + 'description': 'Pick the script for pms down', + 'input_type': 'select', + 'select_options': self.list_scripts() + }, + {'label': 'Plex Server Up', + 'value': plexpy.CONFIG.SCRIPTS_ON_INTUP_SCRIPT, + 'name': 'scripts_on_intup_script', + 'description': 'Pick the script for pms up', + 'input_type': 'select', + 'select_options': self.list_scripts() + } + ] + + return config_option diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 39bc34aa..b021862c 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -476,6 +476,7 @@ class WebInterface(object): "notify_on_extup_body_text": plexpy.CONFIG.NOTIFY_ON_EXTUP_BODY_TEXT, "notify_on_intup_subject_text": plexpy.CONFIG.NOTIFY_ON_INTUP_SUBJECT_TEXT, "notify_on_intup_body_text": plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT, + "notify_scripts_args_text": plexpy.CONFIG.NOTIFY_SCRIPTS_ARGS_TEXT, "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE), "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, @@ -554,7 +555,7 @@ class WebInterface(object): # Get new server URLs for SSL communications. plextv.get_real_pms_url() - + # Get new server friendly name pmsconnect.get_server_friendly_name() @@ -662,6 +663,7 @@ class WebInterface(object): @cherrypy.expose def test_notifier(self, config_id=None, subject='PlexPy', body='Test notification', **kwargs): cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + print kwargs if config_id.isdigit(): agents = notifiers.available_notification_agents() @@ -671,10 +673,10 @@ class WebInterface(object): break else: this_agent = None - + if this_agent: logger.debug("Sending test %s notification." % this_agent['name']) - notifiers.send_notification(this_agent['id'], subject, body) + notifiers.send_notification(this_agent['id'], subject, body, **kwargs) return "Notification sent." else: logger.debug("Unable to send test notification, invalid notification agent ID %s." % config_id) @@ -682,7 +684,7 @@ class WebInterface(object): else: logger.debug("Unable to send test notification, no notification agent ID received.") return "No notification agent ID received." - + @cherrypy.expose def twitterStep1(self): cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" @@ -1339,12 +1341,12 @@ class WebInterface(object): plexpy.CONFIG.__setattr__('PMS_SSL', ssl) plexpy.CONFIG.__setattr__('PMS_IS_REMOTE', remote) plexpy.CONFIG.write() - + plextv.get_real_pms_url() - + pms_connect = pmsconnect.PmsConnect() request = pms_connect.get_local_server_identity() - + if request: cherrypy.response.headers['Content-type'] = 'application/xml' return request @@ -1420,6 +1422,12 @@ class WebInterface(object): return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers", data=this_agent) + @cherrypy.expose + def testScripts(self, *args, **kwargs): + ''' Used for manual testing for now cba with adding buttion ''' + script = notifiers.Scripts() + return script.test(*args, **kwargs) + @cherrypy.expose def delete_history_rows(self, row_id, **kwargs): data_factory = datafactory.DataFactory()