diff --git a/data/interfaces/default/css/plexwatch.css b/data/interfaces/default/css/plexwatch.css index 5188978d..a11331a6 100644 --- a/data/interfaces/default/css/plexwatch.css +++ b/data/interfaces/default/css/plexwatch.css @@ -8416,24 +8416,23 @@ ol.test >li { background-color: #2f2f2f; } -.stacked-configs > li > span > a { +.stacked-configs > li > span > a.toggle-right { float: right; color: #999; padding-left: 10px; } +.stacked-configs > li > span > a.toggle-left { + color: #444; + padding-right: 2px; +} + .stacked-configs > li > span > a:hover { color: #eee; } -.stacked-configs > li > span > i { - cursor: pointer; - padding-right: 2px; - color: #444; -} - -.stacked-configs > li > span > i:hover { - color: #eee; +.stacked-configs > li > span > a.active { + color: #eb8600; } .stacked-configs > li > span > input[type='checkbox'] { diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index f364d80b..a0541c4c 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -99,7 +99,11 @@ DOCUMENTATION :: END
+ % if a['user_id']: + ${a['friendly_name']} is ${a['state']} + % else: ${a['friendly_name']} is ${a['state']} + % endif
% if a['type'] == 'episode': diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index f55b7b6d..fdca4cb8 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -80,6 +80,7 @@

Web Interface

+

Web interface changes require a restart.

@@ -116,6 +117,7 @@

Authentication

+

Authentication changes require a restart.

@@ -273,7 +275,7 @@
-

Global Notifications

+

Global Notification Toggles

Enable Movie and TV Notifications @@ -282,6 +284,16 @@ Enable Music Notifications
+
+

Notification Tuning

+
+
+
+ + +

Set the progress percentage of when a watched notification should be triggered. Minimum 50, Maximum 95.

+
+
@@ -299,12 +311,12 @@
  • - - - + + + ${agent['name']} % if agent['has_config']: - + % endif
  • @@ -661,10 +673,7 @@ $('.notify-toggle-icon').each(function() { if ($(this).data('config-value') == 1) { - $(this).css("color", "#eb8600"); $(this).addClass("active"); - } else { - $(this).css("color", "#444"); } }); @@ -679,7 +688,6 @@ data: data, async: true, success: function(data) { - toggle.css("color", "#444"); toggle.removeClass("active"); } }); @@ -691,7 +699,6 @@ data: data, async: true, success: function(data) { - toggle.css("color", "#eb8600"); toggle.addClass("active"); } }); diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 5840120d..947d10fd 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -524,6 +524,13 @@ def dbcheck(): 'ALTER TABLE session_history_metadata ADD COLUMN full_title TEXT' ) + # notify_log table :: This is a table which logs notifications sent + c_db.execute( + 'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, ' + 'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER)' + ) + conn_db.commit() c_db.close() diff --git a/plexpy/config.py b/plexpy/config.py index 3d4d32b7..bcd3ed81 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -103,6 +103,7 @@ _CONFIG_DEFINITIONS = { 'NMA_ON_PLAY': (int, 'NMA', 0), 'NMA_ON_STOP': (int, 'NMA', 0), 'NMA_ON_WATCHED': (int, 'NMA', 0), + 'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85), 'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'), 'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0), @@ -128,6 +129,7 @@ _CONFIG_DEFINITIONS = { 'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0), 'PUSHBULLET_APIKEY': (str, 'PushBullet', ''), 'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''), + 'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''), 'PUSHBULLET_ENABLED': (int, 'PushBullet', 0), 'PUSHBULLET_ON_PLAY': (int, 'PushBullet', 0), 'PUSHBULLET_ON_STOP': (int, 'PushBullet', 0), diff --git a/plexpy/helpers.py b/plexpy/helpers.py index 4a5767c1..f864ebc2 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -99,7 +99,7 @@ def latinToAscii(unicrap): pass else: r += str(i) - return r + return r.encode('utf-8') def convert_milliseconds(ms): @@ -352,7 +352,7 @@ def convert_xml_to_dict(xml): def get_percent(value1, value2): - if value1.isdigit() and value2.isdigit(): + if str(value1).isdigit() and str(value2).isdigit(): value1 = cast_to_float(value1) value2 = cast_to_float(value2) else: diff --git a/plexpy/monitor.py b/plexpy/monitor.py index ea5fcd2c..fd662d94 100644 --- a/plexpy/monitor.py +++ b/plexpy/monitor.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, pmsconnect, notification_handler, log_reader, common, database +from plexpy import logger, pmsconnect, notification_handler, log_reader, common, database, helpers import threading import plexpy @@ -97,8 +97,10 @@ def check_active_sessions(): # Here we can check the play states if session['state'] != stream['state']: if session['state'] == 'paused': - # Push any notifications - notification_handler.notify(stream_data=stream, notify_action='pause') + # Push any notifications - + # Push it on it's own thread so we don't hold up our db actions + threading.Thread(target=notification_handler.notify, + kwargs=dict(stream_data=stream, notify_action='pause')).start() if stream['state'] == 'paused': # The stream is still paused so we need to increment the paused_counter # Using the set config parameter as the interval, probably not the most accurate but @@ -107,14 +109,33 @@ def check_active_sessions(): monitor_db.action('UPDATE sessions SET paused_counter = ? ' 'WHERE session_key = ? AND rating_key = ?', [paused_counter, stream['session_key'], stream['rating_key']]) + # Check if the user has reached the offset in the media we defined as the "watched" percent + if session['progress'] and session['duration']: + if helpers.get_percent(session['progress'], session['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: + # Push any notifications - + # Push it on it's own thread so we don't hold up our db actions + threading.Thread(target=notification_handler.notify, + kwargs=dict(stream_data=stream, notify_action='watched')).start() + else: # The user has stopped playing a stream logger.debug(u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue" % (stream['session_key'], stream['rating_key'])) monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) - # Push any notifications - notification_handler.notify(stream_data=stream, notify_action='stop') + + # Check if the user has reached the offset in the media we defined as the "watched" percent + if stream['view_offset'] and stream['duration']: + if helpers.get_percent(stream['view_offset'], stream['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: + # Push any notifications - + # Push it on it's own thread so we don't hold up our db actions + threading.Thread(target=notification_handler.notify, + kwargs=dict(stream_data=stream, notify_action='watched')).start() + + # Push any notifications - Push it on it's own thread so we don't hold up our db actions + threading.Thread(target=notification_handler.notify, + kwargs=dict(stream_data=stream, notify_action='stop')).start() + # Write the item history on playback stop monitor_process.write_session_history(session=stream) @@ -132,7 +153,8 @@ class MonitorProcessing(object): def write_session(self, session=None): - values = {'rating_key': session['rating_key'], + values = {'session_key': session['session_key'], + 'rating_key': session['rating_key'], 'media_type': session['type'], 'state': session['state'], 'user_id': session['user_id'], @@ -175,8 +197,10 @@ class MonitorProcessing(object): result = self.db.upsert('sessions', values, keys) if result == 'insert': - # Push any notifications - notification_handler.notify(stream_data=values, notify_action='play') + # Push any notifications - Push it on it's own thread so we don't hold up our db actions + threading.Thread(target=notification_handler.notify, + kwargs=dict(stream_data=values,notify_action='play')).start() + started = int(time.time()) # Try and grab IP address from logs diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index 3b099789..66dab406 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -13,9 +13,10 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, config, notifiers +from plexpy import logger, config, notifiers, database import plexpy +import time def notify(stream_data=None, notify_action=None): from plexpy import pmsconnect, common @@ -37,7 +38,7 @@ def notify(stream_data=None, notify_action=None): item_title = stream_data['title'] if notify_action == 'play': - logger.info('PlexPy Monitor :: %s (%s) started playing %s.' % (stream_data['friendly_name'], + logger.info('PlexPy Notifier :: %s (%s) started playing %s.' % (stream_data['friendly_name'], stream_data['player'], item_title)) if stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode': @@ -45,35 +46,101 @@ def notify(stream_data=None, notify_action=None): for agent in notifiers.available_notification_agents(): if agent['on_play'] and notify_action == 'play': - logger.debug("%s agent is configured to notify on playback start." % agent['name']) + logger.debug("PlexPy Notifier :: %s agent is configured to notify on playback start." % agent['name']) message = '%s (%s) started playing %s.' % \ (stream_data['friendly_name'], stream_data['player'], item_title) notifiers.send_notification(config_id=agent['id'], subject=notify_header, body=message) + set_notify_state(session=stream_data, state='play', agent_info=agent) elif agent['on_stop'] and notify_action == 'stop': - logger.debug("%s agent is configured to notify on playback stop." % agent['name']) + logger.debug("PlexPy Notifier :: %s agent is configured to notify on playback stop." % agent['name']) message = '%s (%s) has stopped %s.' % \ (stream_data['friendly_name'], stream_data['player'], item_title) notifiers.send_notification(config_id=agent['id'], subject=notify_header, body=message) + set_notify_state(session=stream_data, state='stop', agent_info=agent) + elif agent['on_watched'] and notify_action == 'watched': + notify_states = get_notify_state(session=stream_data) + # If there is nothing in the notify_log for our agent id but it is enabled we should notify + if not any(d['agent_id'] == agent['id'] for d in notify_states): + logger.debug("PlexPy Notifier :: %s agent is configured to notify on watched." % agent['name']) + message = '%s (%s) has watched %s.' % \ + (stream_data['friendly_name'], stream_data['player'], item_title) + notifiers.send_notification(config_id=agent['id'], subject=notify_header, body=message) + set_notify_state(session=stream_data, state='watched', agent_info=agent) + else: + # Check in our notify log if the notification has already been sent + for notify_state in notify_states: + if not notify_state['on_watched'] and (notify_state['agent_id'] == agent['id']): + logger.debug("PlexPy Notifier :: %s agent is configured to notify on watched." % agent['name']) + message = '%s (%s) has watched %s.' % \ + (stream_data['friendly_name'], stream_data['player'], item_title) + notifiers.send_notification(config_id=agent['id'], subject=notify_header, body=message) + set_notify_state(session=stream_data, state='watched', agent_info=agent) elif stream_data['media_type'] == 'track': if plexpy.CONFIG.MUSIC_NOTIFY_ENABLE: for agent in notifiers.available_notification_agents(): if agent['on_play'] and notify_action == 'play': - logger.debug("%s agent is configured to notify on playback start." % agent['name']) + logger.debug("PlexPy Notifier :: %s agent is configured to notify on playback start." % agent['name']) message = '%s (%s) started playing %s.' % \ (stream_data['friendly_name'], stream_data['player'], item_title) notifiers.send_notification(config_id=agent['id'], subject=notify_header, body=message) + set_notify_state(session=stream_data, state='play', agent_info=agent) elif agent['on_stop'] and notify_action == 'stop': - logger.debug("%s agent is configured to notify on playback stop." % agent['name']) + logger.debug("PlexPy Notifier :: %s agent is configured to notify on playback stop." % agent['name']) message = '%s (%s) has stopped %s.' % \ (stream_data['friendly_name'], stream_data['player'], item_title) notifiers.send_notification(config_id=agent['id'], subject=notify_header, body=message) + set_notify_state(session=stream_data, state='stop', agent_info=agent) elif stream_data['media_type'] == 'clip': pass else: - logger.debug(u"PlexPy Monitor :: Notify called with unsupported media type.") + logger.debug(u"PlexPy Notifier :: Notify called with unsupported media type.") pass else: - logger.debug(u"PlexPy Monitor :: Notify called but incomplete data received.") + logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.") + +def get_notify_state(session): + monitor_db = database.MonitorDatabase() + result = monitor_db.select('SELECT on_play, on_stop, on_watched, agent_id ' + 'FROM notify_log ' + 'WHERE session_key = ? ' + 'AND rating_key = ? ' + 'AND user = ? ' + 'ORDER BY id DESC', + args=[session['session_key'], session['rating_key'], session['user']]) + notify_states = [] + for item in result: + notify_state = {'on_play': item[0], + 'on_stop': item[1], + 'on_watched': item[2], + 'agent_id': item[3]} + notify_states.append(notify_state) + + return notify_states + +def set_notify_state(session, state, agent_info): + + if session and state and agent_info: + monitor_db = database.MonitorDatabase() + + if state == 'play': + values = {'on_play': int(time.time())} + elif state == 'stop': + values = {'on_stop': int(time.time())} + elif state == 'watched': + values = {'on_watched': int(time.time())} + else: + return + + keys = {'session_key': session['session_key'], + 'rating_key': session['rating_key'], + 'user_id': session['user_id'], + 'user': session['user'], + 'agent_id': agent_info['id'], + 'agent_name': agent_info['name']} + + monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values) + else: + logger.error('PlexPy Notifier :: Unable to set notify state.') diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index 855380de..dae66bb4 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -44,9 +44,9 @@ AGENT_IDS = {"Growl": 0, "XBMC": 2, "Plex": 3, "NMA": 4, - "PushAlot": 5, - "PushBullet": 6, - "PushOver": 7, + "Pushalot": 5, + "Pushbullet": 6, + "Pushover": 7, "OSX Notify": 8, "Boxcar2": 9, "Email": 10} @@ -97,8 +97,8 @@ def available_notification_agents(): 'on_stop': plexpy.CONFIG.NMA_ON_STOP, 'on_watched': plexpy.CONFIG.NMA_ON_WATCHED }, - {'name': 'PushAlot', - 'id': AGENT_IDS['PushAlot'], + {'name': 'Pushalot', + 'id': AGENT_IDS['Pushalot'], 'config_prefix': 'pushalot', 'has_config': True, 'state': checked(plexpy.CONFIG.PUSHALOT_ENABLED), @@ -106,8 +106,8 @@ def available_notification_agents(): 'on_stop': plexpy.CONFIG.PUSHALOT_ON_STOP, 'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED }, - {'name': 'PushBullet', - 'id': AGENT_IDS['PushBullet'], + {'name': 'Pushbullet', + 'id': AGENT_IDS['Pushbullet'], 'config_prefix': 'pushbullet', 'has_config': True, 'state': checked(plexpy.CONFIG.PUSHBULLET_ENABLED), @@ -115,8 +115,8 @@ def available_notification_agents(): 'on_stop': plexpy.CONFIG.PUSHBULLET_ON_STOP, 'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED }, - {'name': 'PushOver', - 'id': AGENT_IDS['PushOver'], + {'name': 'Pushover', + 'id': AGENT_IDS['Pushover'], 'config_prefix': 'pushover', 'has_config': True, 'state': checked(plexpy.CONFIG.PUSHOVER_ENABLED), @@ -257,7 +257,7 @@ class GROWL(object): return cherrypy.config['config'].get('Growl', options) def notify(self, message, event): - if not self.enabled: + if not message or not event: return # Split host and port @@ -362,7 +362,7 @@ class PROWL(object): return cherrypy.config['config'].get('Prowl', options) def notify(self, message, event): - if not plexpy.CONFIG.PROWL_ENABLED: + if not message or not event: return http_handler = HTTPSConnection("api.prowlapp.com") @@ -596,18 +596,21 @@ class NMA(object): self.on_watched = plexpy.CONFIG.NMA_ON_WATCHED def notify(self, subject=None, message=None): + if not subject or not message: + return + title = 'PlexPy' api = plexpy.CONFIG.NMA_APIKEY nma_priority = plexpy.CONFIG.NMA_PRIORITY - logger.debug(u"NMA title: " + title) - logger.debug(u"NMA API: " + api) - logger.debug(u"NMA Priority: " + str(nma_priority)) + # logger.debug(u"NMA title: " + title) + # logger.debug(u"NMA API: " + api) + # logger.debug(u"NMA Priority: " + str(nma_priority)) event = subject - logger.debug(u"NMA event: " + event) - logger.debug(u"NMA message: " + message) + # logger.debug(u"NMA event: " + event) + # logger.debug(u"NMA message: " + message) batch = False @@ -648,6 +651,7 @@ class PUSHBULLET(object): def __init__(self): self.apikey = plexpy.CONFIG.PUSHBULLET_APIKEY self.deviceid = plexpy.CONFIG.PUSHBULLET_DEVICEID + self.channel_tag = plexpy.CONFIG.PUSHBULLET_CHANNEL_TAG self.on_play = plexpy.CONFIG.PUSHBULLET_ON_PLAY self.on_stop = plexpy.CONFIG.PUSHBULLET_ON_STOP self.on_watched = plexpy.CONFIG.PUSHBULLET_ON_WATCHED @@ -665,16 +669,22 @@ class PUSHBULLET(object): 'title': subject.encode("utf-8"), 'body': message.encode("utf-8")} + # Can only send to a device or channel, not both. + if self.deviceid: + data['device_iden'] = self.deviceid + elif self.channel_tag: + 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 + ":")}, + '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) - logger.debug(u"PushBullet response headers: %r" % response.getheaders()) - logger.debug(u"PushBullet response body: %r" % response.read()) + # logger.debug(u"PushBullet response status: %r" % request_status) + # logger.debug(u"PushBullet response headers: %r" % response.getheaders()) + # logger.debug(u"PushBullet response body: %r" % response.read()) if request_status == 200: logger.info(u"PushBullet notifications sent.") @@ -704,7 +714,13 @@ class PUSHBULLET(object): {'label': 'Device ID', 'value': self.deviceid, 'name': 'pushbullet_deviceid', - 'description': 'A device ID (optional).', + 'description': 'A device ID (optional). If set, will override channel tag.', + 'input_type': 'text' + }, + {'label': 'Channel', + 'value': self.channel_tag, + 'name': 'pushbullet_channel_tag', + 'description': 'A channel tag (optional).', 'input_type': 'text' } ] @@ -720,7 +736,7 @@ class PUSHALOT(object): self.on_watched = plexpy.CONFIG.PUSHALOT_ON_WATCHED def notify(self, message, event): - if not plexpy.CONFIG.PUSHALOT_ENABLED: + if not message or not event: return pushalot_authorizationtoken = plexpy.CONFIG.PUSHALOT_APIKEY @@ -786,7 +802,7 @@ class PUSHOVER(object): return cherrypy.config['config'].get('Pushover', options) def notify(self, message, event): - if not plexpy.CONFIG.PUSHOVER_ENABLED: + if not message or not event: return http_handler = HTTPSConnection("api.pushover.net") @@ -1037,11 +1053,11 @@ class BOXCAR(object): self.on_stop = plexpy.CONFIG.BOXCAR_ON_STOP self.on_watched = plexpy.CONFIG.BOXCAR_ON_WATCHED - def notify(self, title, message, rgid=None): - try: - if rgid: - message += '

    MusicBrainz' % rgid + def notify(self, title, message): + if not title or not message: + return + try: data = urllib.urlencode({ 'user_credentials': plexpy.CONFIG.BOXCAR_TOKEN, 'notification[title]': title.encode('utf-8'), @@ -1077,6 +1093,8 @@ class Email(object): self.on_watched = plexpy.CONFIG.EMAIL_ON_WATCHED def notify(self, subject, message): + if not subject or not message: + return message = MIMEText(message, 'plain', "utf-8") message['Subject'] = subject diff --git a/plexpy/webserve.py b/plexpy/webserve.py index d644434e..679d788b 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -510,7 +510,8 @@ class WebInterface(object): "video_logging_enable": checked(plexpy.CONFIG.VIDEO_LOGGING_ENABLE), "music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE), "logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL, - "pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE) + "pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE), + "notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT } return serve_template(templatename="settings.html", title="Settings", config=config)