From 36c2ddaaf6ba696b36edc05b0f0dbda3b7de761c Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 20:13:25 -0700 Subject: [PATCH 01/20] Fix PEP8 issues Some minor refactoring to follow PEP8 guidlines. --- killstream/kill_stream.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 904d82f..31b178b 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -31,7 +31,9 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments: Select: Playback Start, Playback Pause Arguments: --jbop SELECTOR --userId {user_id} --username {username} - --sessionId {session_id} --killMessage Your message here. No quotes. --notify notifierID + --sessionId {session_id} + --killMessage Your message here. No quotes. + --notify notifierID Save Close @@ -53,9 +55,10 @@ BODY_TEXT = "Killed {user}'s stream. Reason: {message}." sess = requests.Session() # Ignore verifying the SSL certificate -sess.verify = False # '/path/to/certfile' +sess.verify = False # '/path/to/certfile' # If verify is set to a path to a directory, -# the directory must have been processed using the c_rehash utility supplied with OpenSSL. +# the directory must have been processed using the c_rehash utility supplied +# with OpenSSL. SELECTOR = ['stream', 'allStreams'] @@ -77,7 +80,8 @@ def send_notification(subject_text, body_text, notifier_id): else: raise Exception(response['response']['message']) except Exception as e: - sys.stderr.write("Tautulli API 'notify' request failed: {0}.".format(e)) + sys.stderr.write( + "Tautulli API 'notify' request failed: {0}.".format(e)) return None @@ -87,15 +91,18 @@ def get_activity(user_id): 'cmd': 'get_activity'} try: - req = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) + req = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', + params=payload) response = req.json() res_data = response['response']['data']['sessions'] - user_streams = [d['session_id'] for d in res_data if d['user_id'] == user_id] + user_streams = [d['session_id'] + for d in res_data if d['user_id'] == user_id] return user_streams except Exception as e: - sys.stderr.write("Tautulli API 'get_activity' request failed: {0}.".format(e)) + sys.stderr.write( + "Tautulli API 'get_activity' request failed: {0}.".format(e)) pass @@ -111,16 +118,19 @@ def terminate_session(session_id, message): response = req.json() if response['response']['result'] == 'success': - sys.stdout.write("Successfully killed Plex session: {0}.".format(session_id)) + sys.stdout.write( + "Successfully killed Plex session: {0}.".format(session_id)) else: raise Exception(response['response']['message']) except Exception as e: - sys.stderr.write("Tautulli API 'terminate_session' request failed: {0}.".format(e)) + sys.stderr.write( + "Tautulli API 'terminate_session' request failed: {0}.".format(e)) return None if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Killing Plex streams from Tautulli.") + parser = argparse.ArgumentParser( + description="Killing Plex streams from Tautulli.") parser.add_argument('--jbop', required=True, choices=SELECTOR, help='Kill selector.\nChoices: (%(choices)s)') parser.add_argument('--userId', type=int, @@ -132,7 +142,8 @@ if __name__ == "__main__": parser.add_argument('--killMessage', nargs='+', help='Message to send to user whose stream is killed.') parser.add_argument('--notify', type=int, - help='Notification Agent ID number to Agent to send notification.') + help='Notification Agent ID number to Agent to send ' + + 'notification.') opts = parser.parse_args() From f7c555caa7a1a983fceb53392ac8a1ea28fe04a5 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 20:59:30 -0700 Subject: [PATCH 02/20] Add docbloc for send_notification --- killstream/kill_stream.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 31b178b..7549898 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -64,7 +64,17 @@ SELECTOR = ['stream', 'allStreams'] def send_notification(subject_text, body_text, notifier_id): - # Send the notification through Tautulli + """Send a notification through Tautulli + + Parameters + ---------- + subject_text : str + The text to use for the subject line of the message. + body_text : str + The text to use for the body of the notification. + notifier_id : int + Tautulli Notification Agent ID to send the notification to. + """ payload = {'apikey': TAUTULLI_APIKEY, 'cmd': 'notify', 'notifier_id': notifier_id, From e56d23b12df77b592a2e784209cb096750766a90 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 21:01:21 -0700 Subject: [PATCH 03/20] Refactor get_activity into two functions Create a new function get_user_activity that returns the same data get_activity originally did and use that in main. get_activity now actually follows the name and gets all activity on the server. --- killstream/kill_stream.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 7549898..a8f94d4 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -95,8 +95,14 @@ def send_notification(subject_text, body_text, notifier_id): return None -def get_activity(user_id): - # Get the current activity on the PMS. +def get_activity(): + """Get the current activity on the PMS. + + Returns + ------- + list + The current active sessions on the Plex server. + """ payload = {'apikey': TAUTULLI_APIKEY, 'cmd': 'get_activity'} @@ -106,9 +112,7 @@ def get_activity(user_id): response = req.json() res_data = response['response']['data']['sessions'] - user_streams = [d['session_id'] - for d in res_data if d['user_id'] == user_id] - return user_streams + return res_data except Exception as e: sys.stderr.write( @@ -116,6 +120,26 @@ def get_activity(user_id): pass +def get_user_activity(user_id): + """Get current sessions for a specific user. + + Parameters + ---------- + user_id : int + The ID of the user to grab sessions for. + + Returns + ------- + list + The active sessions for the specific user ID. + + """ + sessions = get_activity() + user_streams = [s['session_id'] + for s in sessions if s['user_id'] == user_id] + return user_streams + + def terminate_session(session_id, message): # Stop a streaming session. payload = {'apikey': TAUTULLI_APIKEY, @@ -165,7 +189,7 @@ if __name__ == "__main__": if opts.jbop == 'stream': terminate_session(opts.sessionId, message) elif opts.jbop == 'allStreams': - streams = get_activity(opts.userId) + streams = get_user_activity(opts.userId) for session_id in streams: terminate_session(session_id, message) From 00f4bf26a925070cc9fb4e487085d73dd3ecdde1 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 21:04:55 -0700 Subject: [PATCH 04/20] Rename get_user_activity This function was actually getting _just_ the session IDs, not the whole session data. --- killstream/kill_stream.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index a8f94d4..2398d01 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -120,8 +120,8 @@ def get_activity(): pass -def get_user_activity(user_id): - """Get current sessions for a specific user. +def get_user_session_ids(user_id): + """Get current session IDs for a specific user. Parameters ---------- @@ -131,7 +131,7 @@ def get_user_activity(user_id): Returns ------- list - The active sessions for the specific user ID. + The active session IDs for the specific user ID. """ sessions = get_activity() @@ -189,7 +189,7 @@ if __name__ == "__main__": if opts.jbop == 'stream': terminate_session(opts.sessionId, message) elif opts.jbop == 'allStreams': - streams = get_user_activity(opts.userId) + streams = get_user_session_ids(opts.userId) for session_id in streams: terminate_session(session_id, message) From 7b0cc2d2a9f5c7777af9c00364ebfebfb09eb874 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 22:23:32 -0700 Subject: [PATCH 05/20] Disable InsecureRequestWarning if SSL verify is False If checking of HTTPS validity is disable, turn off the warning that it has been disabled since we have done so explicitly. --- killstream/kill_stream.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 2398d01..fa38144 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -59,6 +59,10 @@ sess.verify = False # '/path/to/certfile' # If verify is set to a path to a directory, # the directory must have been processed using the c_rehash utility supplied # with OpenSSL. +if sess.verify is False: + # Disable the warning that the request is insecure, we know that... + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) SELECTOR = ['stream', 'allStreams'] From 342284173bae9c776e3e75642a903324ead3bd92 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 22:24:48 -0700 Subject: [PATCH 06/20] Use the same session for all requests Use the Session created at the start of the script for all requests, instead of creating a new one for some of them (which doesn't inherit the settings set on it at the start). --- killstream/kill_stream.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index fa38144..a7ee28d 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -86,7 +86,7 @@ def send_notification(subject_text, body_text, notifier_id): 'body': body_text} try: - r = requests.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) + r = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) response = r.json() if response['response']['result'] == 'success': @@ -111,8 +111,7 @@ def get_activity(): 'cmd': 'get_activity'} try: - req = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', - params=payload) + req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) response = req.json() res_data = response['response']['data']['sessions'] From 2116082fd7f1a8095ce5f85927f0b3edbc3d6273 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 22:26:58 -0700 Subject: [PATCH 07/20] List --killMessage at the end Although argparse should be smart enough to handle this in any order, it's generally easier for us poor humans to read the arguments if the unstructured one is the last argument. Re-order the listing to make this clearer. --- killstream/kill_stream.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index a7ee28d..f6a856a 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -31,9 +31,8 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments: Select: Playback Start, Playback Pause Arguments: --jbop SELECTOR --userId {user_id} --username {username} - --sessionId {session_id} + --sessionId {session_id} --notify notifierID --killMessage Your message here. No quotes. - --notify notifierID Save Close @@ -176,11 +175,11 @@ if __name__ == "__main__": help='The username of the person streaming.') parser.add_argument('--sessionId', required=True, help='The unique identifier for the stream.') - parser.add_argument('--killMessage', nargs='+', - help='Message to send to user whose stream is killed.') parser.add_argument('--notify', type=int, help='Notification Agent ID number to Agent to send ' + 'notification.') + parser.add_argument('--killMessage', nargs='+', + help='Message to send to user whose stream is killed.') opts = parser.parse_args() From 4ebfaf17bd8bc4387e6221b01c84a91d6cd0c871 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 23:15:59 -0700 Subject: [PATCH 08/20] Always send a notification if an agent is specified Whenever a stream is killed, send a notification for that stream if a notification agent has been specified. If no username is specified, simply list the session ID. --- killstream/kill_stream.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index f6a856a..ecef18f 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -50,7 +50,8 @@ TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL) TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY) SUBJECT_TEXT = "Tautulli has killed a stream." -BODY_TEXT = "Killed {user}'s stream. Reason: {message}." +BODY_TEXT = "Killed session ID '{id}'. Reason: {message}" +BODY_TEXT_USER = "Killed {user}'s stream. Reason: {message}." sess = requests.Session() # Ignore verifying the SSL certificate @@ -142,7 +143,7 @@ def get_user_session_ids(user_id): return user_streams -def terminate_session(session_id, message): +def terminate_session(session_id, message, notifier=None, username=None): # Stop a streaming session. payload = {'apikey': TAUTULLI_APIKEY, 'cmd': 'terminate_session', @@ -156,6 +157,12 @@ def terminate_session(session_id, message): if response['response']['result'] == 'success': sys.stdout.write( "Successfully killed Plex session: {0}.".format(session_id)) + if notifier: + if username: + body = BODY_TEXT.format(user=username, message=message) + else: + body = BODY_TEXT.format(id=session_id, message=message) + send_notification(SUBJECT_TEXT, body, notifier) else: raise Exception(response['response']['message']) except Exception as e: @@ -189,12 +196,8 @@ if __name__ == "__main__": message = '' if opts.jbop == 'stream': - terminate_session(opts.sessionId, message) + terminate_session(opts.sessionId, message, opts.notify, opts.username) elif opts.jbop == 'allStreams': streams = get_user_session_ids(opts.userId) for session_id in streams: - terminate_session(session_id, message) - - if opts.notify: - BODY_TEXT = BODY_TEXT.format(user=opts.username, message=message) - send_notification(SUBJECT_TEXT, BODY_TEXT, opts.notify) + terminate_session(session_id, message, opts.notify, opts.username) From 0ba8e2bda01f022ed29cedc92194b57baec00a7b Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 23:19:30 -0700 Subject: [PATCH 09/20] Add a docstring for terminate_session --- killstream/kill_stream.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index ecef18f..310d353 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -144,7 +144,19 @@ def get_user_session_ids(user_id): def terminate_session(session_id, message, notifier=None, username=None): - # Stop a streaming session. + """Stop a streaming session. + + Parameters + ---------- + session_id : str + The session ID of the stream to terminate. + message : str + The message to display to the user when terminating a stream. + notifier : int + Notification agent ID to send a message to (the default is None). + username : str + The username for the terminated session (the default is None). + """ payload = {'apikey': TAUTULLI_APIKEY, 'cmd': 'terminate_session', 'session_id': session_id, From c03860ec70df5d35edb02c35b2dc3c4d0a1a2039 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sat, 16 Jun 2018 23:36:27 -0700 Subject: [PATCH 10/20] Incorporate waiting to kill paused streams into kill_stream.py Incorporate the functionality of `wait_kill_paused_notify.py` into `kill_stream.py`. A new selector is added of `paused` enabling this mode, and adds two additional arguments to control how long to wait for paused streams, and how often to check a stream's status. The default values are to kill a stream after 20 minutes, checking every 30 seconds. The Tautulli API is used for all functionality, meaning the `plexapi` module is no longer required. --- killstream/kill_stream.py | 65 +++++++++++++- killstream/wait_kill_paused_notify.py | 119 -------------------------- 2 files changed, 64 insertions(+), 120 deletions(-) delete mode 100644 killstream/wait_kill_paused_notify.py diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 310d353..22c92ae 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -32,6 +32,7 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments: Select: Playback Start, Playback Pause Arguments: --jbop SELECTOR --userId {user_id} --username {username} --sessionId {session_id} --notify notifierID + --interval 30 --limit 1200 --killMessage Your message here. No quotes. Save @@ -43,6 +44,8 @@ import requests import argparse import sys import os +from time import sleep +from datetime import datetime TAUTULLI_URL = '' TAUTULLI_APIKEY = '' @@ -64,7 +67,7 @@ if sess.verify is False: import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -SELECTOR = ['stream', 'allStreams'] +SELECTOR = ['stream', 'allStreams', 'paused'] def send_notification(subject_text, body_text, notifier_id): @@ -183,6 +186,59 @@ def terminate_session(session_id, message, notifier=None, username=None): return None +def terminate_long_pause(session_id, message, limit, interval, notify=None): + """Kills the session if it is paused for longer than seconds. + + Parameters + ---------- + session_id : str + The session id of the session to monitor. + message : str + The message to use if the stream is terminated. + limit : int + The number of seconds the session is allowed to remain paused before it + is terminated. + interval : int + The amount of time to wait between checks of the session state. + notify : int + Tautulli Notification Agent ID to send a notification to on killing a + stream. + """ + start = datetime.now() + fudgeFactor = 100 # Keep checking this long after the defined limit + pausedTime = 0 + checkLimit = limit + interval + fudgeFactor + + while pausedTime < checkLimit: + sessions = get_activity() + foundSession = False + + for session in sessions: + if session['session_id'] == session_id: + foundSession = True + state = session['state'] + + if state == 'paused': + now = datetime.now() + diff = now - start + + if diff.total_seconds() >= limit: + terminate_session(session_id, message, notify) + sys.exit(0) + else: + sleep(interval) + elif state == 'playing' or state == 'buffering': + sys.stdout.write( + "Session '{}' has resumed, ".format(session_id) + + "stopping monitoring.") + sys.exit(0) + if not foundSession: + sys.stdout.write( + "Session '{}' is no longer active ".format(session_id) + + "on the server, stopping monitoring.") + sys.exit(0) + + if __name__ == "__main__": parser = argparse.ArgumentParser( description="Killing Plex streams from Tautulli.") @@ -197,6 +253,10 @@ if __name__ == "__main__": parser.add_argument('--notify', type=int, help='Notification Agent ID number to Agent to send ' + 'notification.') + parser.add_argument('--limit', type=int, default=(20 * 60), # 20 minutes + help='The time session is allowed to remain paused.') + parser.add_argument('--interval', type=int, default=30, + help='The seconds between paused session checks.') parser.add_argument('--killMessage', nargs='+', help='Message to send to user whose stream is killed.') @@ -213,3 +273,6 @@ if __name__ == "__main__": streams = get_user_session_ids(opts.userId) for session_id in streams: terminate_session(session_id, message, opts.notify, opts.username) + elif opts.jbop == 'paused': + terminate_long_pause(opts.sessionId, message, opts.limit, + opts.interval, opts.notify) diff --git a/killstream/wait_kill_paused_notify.py b/killstream/wait_kill_paused_notify.py deleted file mode 100644 index c0f6996..0000000 --- a/killstream/wait_kill_paused_notify.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Description: Kill paused sessions if paused for X amount of time. -Author: samwiseg00 -Requires: requests, plexapi - -Enabling Scripts in Tautulli: -Taultulli > Settings > Notification Agents > Add a Notification Agent > Script - -Configuration: -Taultulli > Settings > Notification Agents > New Script > Configuration: - - Script Name: wait_kill_notify.py - Set Script Timeout: 0 - Description: Killing long pauses - Save - -Triggers: -Taultulli > Settings > Notification Agents > New Script > Triggers: - - Check: Playback Pause - Save - -Conditions: -Taultulli > Settings > Notification Agents > New Script > Conditions: - - Set Conditions: Condition {1} | Username | is not | UsernameToExclude - Save - -Script Arguments: -Taultulli > Settings > Notification Agents > New Script > Script Arguments: - - Select: Playback Pause - Arguments: {session_key} {user} {title} TIMEOUT INTERVAL - - Save - Close - -Example: - {session_key} {user} {title} 1200 20 - This will tell the script to kill the stream after 20 minutes and check every 20 seconds - -""" - -import os -import sys -from time import sleep -from datetime import datetime -from plexapi.server import PlexServer -import requests - -PLEX_FALLBACK_URL = 'http://127.0.0.1:32400' -PLEX_FALLBACK_TOKEN = '' -PLEX_URL = os.getenv('PLEX_URL', PLEX_FALLBACK_URL) -PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_FALLBACK_TOKEN) - -PLEX_OVERRIDE_URL = '' -PLEX_OVERRIDE_TOKEN = '' - -if PLEX_OVERRIDE_URL: - PLEX_URL = PLEX_OVERRIDE_URL -if PLEX_OVERRIDE_TOKEN: - PLEX_TOKEN = PLEX_OVERRIDE_TOKEN - - -sess = requests.Session() -sess.verify = False -plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess) - -sessionKey = sys.argv[1] -username = sys.argv[2] -streamTitle = sys.argv[3] -timeout = int(sys.argv[4]) -interval = int(sys.argv[5]) - -seconds = int(timeout) - -minutes, seconds = divmod(seconds, 60) -hours, minutes = divmod(minutes, 60) - -periods = [('hours', hours), ('minutes', minutes), ('seconds', seconds)] -time_string = ', '.join('{} {}'.format(value, name) - for name, value in periods - if value) -start = datetime.now() - -countdown = 0 -counter = timeout + interval + 100 - -while countdown < counter and countdown is not None: - - foundSession = False - - for session in plex.sessions(): - - if session.sessionKey == int(sessionKey): - foundSession = True - state = session.players[0].state - - if state == 'paused': - now = datetime.now() - diff = now - start - - if diff.total_seconds() >= timeout: - session.stop(reason="This stream has ended due to being paused for over {}.".format(time_string)) - print ("Killed {}'s {} paused stream of {}.".format(username, time_string, streamTitle)) - sys.exit(0) - - else: - sleep(interval) - counter = counter - interval - - elif state == 'playing' or state == 'buffering': - print ("{} resumed the stream of {} so we killed the script.".format(username, streamTitle)) - sys.exit(0) - - if not foundSession: - print ("Session key ({}) for user {} not found while playing {}. " - "The player may have gone to a paused then stopped state.".format(sessionKey, username, streamTitle)) - sys.exit(0) From 98b72f0a621952dc54b08fe900d2dfab5883783e Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Sun, 17 Jun 2018 00:27:13 -0700 Subject: [PATCH 11/20] Cleanup documentation and add examples Cleanup the documentation on how to set it up to match the current Tautulli state. Add a few examples to give people ideas for how to use this script. --- killstream/kill_stream.py | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 22c92ae..8f17ea1 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -3,21 +3,23 @@ Description: Use conditions to kill a stream Author: Blacktwin Requires: requests -Enabling Scripts in Tautulli: -Taultulli > Settings > Notification Agents > Add a Notification Agent > Script +Adding the script to Tautulli: +Taultulli > Settings > Notification Agents > Add a new notification agent > + Script Configuration: Taultulli > Settings > Notification Agents > New Script > Configuration: - Script Name: kill_stream - Set Script Timeout: {timeout} + Script Folder: /path/to/your/scripts + Script File: ./kill_stream.py (Should be selectable in a dropdown list) + Script Timeout: {timeout} Description: Kill stream(s) Save Triggers: Taultulli > Settings > Notification Agents > New Script > Triggers: - Check: {trigger} + Check: Playback Start and/or Playback Pause Save Conditions: @@ -38,6 +40,40 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments: Save Close +Examples: + +Kill any transcoding streams +Script Timeout: 30 (default) +Triggers: Playback Start +Conditions: Transcode Decision is transcode +Arguments (Playback Start): --jbop stream --sessionId {session_id} + --username {username} + --killMessage Transcoding streams are not allowed on this server. + +Kill all streams started by a specific user +Script Timeout: 30 (default) +Triggers: Playback Start +Conditions: Username is Bob +Arguments (Playback Start): --jbop allStreams --userId {user_id} + --username {username} + --killMessage Hey Bob, we need to talk! + +Kill all WAN streams paused longer than 20 minutes, checking every 30 seconds +Script Timeout: 0 +Triggers: Playback Pause +Conditions: Stream Location is not LAN +Arguments (Playback Start): --jbop paused --sessionId {session_id} + --limit 1200 --interval 30 + --killMessage Your stream was paused for over 20 minutes and has been + automatically stopped for you. + +Notes: +* Any of these can have "--notify X" added to them, where X is the +ID shown in Tautulli for a Notification agent, if specified it will send a +message there when a stream is terminated. +* Arguments should all be on one line in Tautulli, they are split here for +easier reading. + """ import requests From 31ddd97a344283237fd81f33b9af494499696049 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 11:50:41 -0700 Subject: [PATCH 12/20] Actually use BODY_TEXT_USER --- killstream/kill_stream.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 8f17ea1..8bd24af 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -210,7 +210,8 @@ def terminate_session(session_id, message, notifier=None, username=None): "Successfully killed Plex session: {0}.".format(session_id)) if notifier: if username: - body = BODY_TEXT.format(user=username, message=message) + body = BODY_TEXT_USER.format(user=username, + message=message) else: body = BODY_TEXT.format(id=session_id, message=message) send_notification(SUBJECT_TEXT, body, notifier) From 99b36aab88ab64b41ee842c1cd5b5211c0ccd8de Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 11:54:17 -0700 Subject: [PATCH 13/20] camelCase -> snake_case --- killstream/kill_stream.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 8bd24af..15674fc 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -242,17 +242,17 @@ def terminate_long_pause(session_id, message, limit, interval, notify=None): stream. """ start = datetime.now() - fudgeFactor = 100 # Keep checking this long after the defined limit - pausedTime = 0 - checkLimit = limit + interval + fudgeFactor + fudge_factor = 100 # Keep checking this long after the defined limit + paused_time = 0 + check_limit = limit + interval + fudge_factor - while pausedTime < checkLimit: + while paused_time < check_limit: sessions = get_activity() - foundSession = False + found_session = False for session in sessions: if session['session_id'] == session_id: - foundSession = True + found_session = True state = session['state'] if state == 'paused': @@ -269,7 +269,7 @@ def terminate_long_pause(session_id, message, limit, interval, notify=None): "Session '{}' has resumed, ".format(session_id) + "stopping monitoring.") sys.exit(0) - if not foundSession: + if not found_session: sys.stdout.write( "Session '{}' is no longer active ".format(session_id) + "on the server, stopping monitoring.") From 1fdae8a10539319f79a599efacabd1d90d858742 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 12:09:05 -0700 Subject: [PATCH 14/20] Minor style cleanup and typos --- killstream/readme.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/killstream/readme.md b/killstream/readme.md index 93067b9..3e3e97d 100644 --- a/killstream/readme.md +++ b/killstream/readme.md @@ -3,17 +3,16 @@ Killing streams is a Plex Pass only feature. So these scripts will only work for Plex Pass users. - ### Kill_stream.py examples: #### Arguments examples: -Kill the one offending stream with a custom message and send notification to notfication agent ID 1 - +Kill the one offending stream with a custom message and send notification to notification agent ID 1 + --jbop stream --userId {user_id} --username {username} --sessionId {session_id} --killMessage You did something wrong. --notify 1 -Kill all the offending users streams with a custom message and send notification to notfication agent ID 1 - +Kill all the offending user's streams with a custom message and send notification to notification agent ID 1 + --jbop allStreams --userId {user_id} --username {username} --sessionId {session_id} --killMessage You did something wrong. --notify 1 Kill the one offending stream with default message @@ -29,53 +28,52 @@ Kill transcodes: Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] Kill paused transcodes: - + Set Trigger: Playback Paused Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] Limit User stream count, kill last stream: - + Set Trigger: Playback Start Set Conditions: [ {User Streams} | {is greater than} | {3} ] IP Whitelist: - + Set Trigger: Playback Start Set Conditions: [ {IP Address} | {is not} | {192.168.0.100 or 192.168.0.101} ] Kill by platform: - + Set Trigger: Playback Start Set Conditions: [ {Platform} | {is} | {Roku or Android} ] Kill transcode by library: - + Set Trigger: Playback Start Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] - [ {Library Name} | {is} | {4K Movies} ] + [ {Library Name} | {is} | {4K Movies} ] Kill transcode by original resolution: - + Set Trigger: Playback Start Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] [ {Video Resolution} | {is} | {1080 or 720}] Kill transcode by bitrate: - + Set Trigger: Playback Start Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] [ {Bitrate} | {is greater than} | {4000} ] Kill by hours of the day: - + Set Trigger: Playback Start Set Conditions: [ {Timestamp} | {begins with} | {09 or 10} ] - # Killing any streams from 9am to 11am + # Killing any streams from 9 AM to 11 AM Kill non local streams: - + Set Trigger: Playback Start Set Conditions: [ {Stream location} | {is} | {wan} ] or Set Conditions: [ {Stream location} | {is not} | {lan} ] - From 4f3e35ef977db6ec742d9c23ae25329b7644e7a9 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 12:09:54 -0700 Subject: [PATCH 15/20] Remove an incorrect condition The stream location can be `CELL` for example, which would count as "local" for the first method of checking. --- killstream/readme.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/killstream/readme.md b/killstream/readme.md index 3e3e97d..55fb092 100644 --- a/killstream/readme.md +++ b/killstream/readme.md @@ -74,6 +74,4 @@ Kill by hours of the day: Kill non local streams: Set Trigger: Playback Start - Set Conditions: [ {Stream location} | {is} | {wan} ] - or Set Conditions: [ {Stream location} | {is not} | {lan} ] From ad682f3d1ba998d847ee97e2f8c59f35cb2983a9 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 12:37:12 -0700 Subject: [PATCH 16/20] Fix time limit on paused stream monitoring The only way that this could ever be hit is if the stream is still active on the server, but the state is NOT one of paused, playing, or buffering. In case Plex decides to change the states in the future having this working properly is a good idea ;) --- killstream/kill_stream.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 15674fc..96d64f8 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -242,11 +242,12 @@ def terminate_long_pause(session_id, message, limit, interval, notify=None): stream. """ start = datetime.now() - fudge_factor = 100 # Keep checking this long after the defined limit - paused_time = 0 - check_limit = limit + interval + fudge_factor + checked_time = 0 + # Continue checking 2 intervals past the allowed limit in order to + # account for system variances. + check_limit = limit + (interval * 2) - while paused_time < check_limit: + while checked_time < check_limit: sessions = get_activity() found_session = False @@ -254,12 +255,11 @@ def terminate_long_pause(session_id, message, limit, interval, notify=None): if session['session_id'] == session_id: found_session = True state = session['state'] + now = datetime.now() + checked_time = (now - start).total_seconds() if state == 'paused': - now = datetime.now() - diff = now - start - - if diff.total_seconds() >= limit: + if checked_time >= limit: terminate_session(session_id, message, notify) sys.exit(0) else: From 0e5c374449efd2e677c843a20ebdc7de9049a7ea Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 12:38:10 -0700 Subject: [PATCH 17/20] Return instead of forcibly exiting This makes no difference currently as the script doesn't do anything after this function finishes, but it allows any cleanup work to happen at the end. --- killstream/kill_stream.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 96d64f8..8db95c0 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -261,19 +261,19 @@ def terminate_long_pause(session_id, message, limit, interval, notify=None): if state == 'paused': if checked_time >= limit: terminate_session(session_id, message, notify) - sys.exit(0) + return else: sleep(interval) elif state == 'playing' or state == 'buffering': sys.stdout.write( "Session '{}' has resumed, ".format(session_id) + "stopping monitoring.") - sys.exit(0) + return if not found_session: sys.stdout.write( "Session '{}' is no longer active ".format(session_id) + "on the server, stopping monitoring.") - sys.exit(0) + return if __name__ == "__main__": From 7b07bb314f2834c527ef3b4e44f4049681b80f3b Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 12:39:21 -0700 Subject: [PATCH 18/20] Update authors --- killstream/kill_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 8db95c0..3b43608 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -1,6 +1,6 @@ """ Description: Use conditions to kill a stream -Author: Blacktwin +Author: Blacktwin, Arcanemagus, samwiseg00 Requires: requests Adding the script to Tautulli: From c976a3dfb1d43b99ff33c2bad0a000e54eb1beb2 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 13:22:40 -0700 Subject: [PATCH 19/20] Cleanup format of examples Cleans up the examples to all have a standard format that covers all required settings and gives example messages where appropriate. --- killstream/kill_stream.py | 16 ---- killstream/readme.md | 154 +++++++++++++++++++++++++++----------- 2 files changed, 109 insertions(+), 61 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 3b43608..480e882 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -42,22 +42,6 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments: Examples: -Kill any transcoding streams -Script Timeout: 30 (default) -Triggers: Playback Start -Conditions: Transcode Decision is transcode -Arguments (Playback Start): --jbop stream --sessionId {session_id} - --username {username} - --killMessage Transcoding streams are not allowed on this server. - -Kill all streams started by a specific user -Script Timeout: 30 (default) -Triggers: Playback Start -Conditions: Username is Bob -Arguments (Playback Start): --jbop allStreams --userId {user_id} - --username {username} - --killMessage Hey Bob, we need to talk! - Kill all WAN streams paused longer than 20 minutes, checking every 30 seconds Script Timeout: 0 Triggers: Playback Pause diff --git a/killstream/readme.md b/killstream/readme.md index 55fb092..a428705 100644 --- a/killstream/readme.md +++ b/killstream/readme.md @@ -1,77 +1,141 @@ -## README +# README -Killing streams is a Plex Pass only feature. So these scripts will only work for Plex Pass users. +Killing streams is a Plex Pass only feature. So these scripts will **only** work for Plex Pass users. +## `kill_stream.py` examples: -### Kill_stream.py examples: +### Kill transcodes -#### Arguments examples: +Triggers: Playback Start +Conditions: \[ `Transcode Decision` | `is` | `transcode` \] -Kill the one offending stream with a custom message and send notification to notification agent ID 1 +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed. +``` - --jbop stream --userId {user_id} --username {username} --sessionId {session_id} --killMessage You did something wrong. --notify 1 +### Kill paused transcodes -Kill all the offending user's streams with a custom message and send notification to notification agent ID 1 +Triggers: Playback Paused +Conditions: \[ `Transcode Decision` | `is` | `transcode` \] - --jbop allStreams --userId {user_id} --username {username} --sessionId {session_id} --killMessage You did something wrong. --notify 1 +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage Paused streams are automatically stopped. +``` -Kill the one offending stream with default message +### Limit User stream count, kill last stream - --jbop stream --userId {user_id} --username {username} --sessionId {session_id} +Triggers: Playback Start +Conditions: \[ `User Streams` | `is greater than` | `3` \] +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage You are only allowed 3 streams. +``` -#### Condition Examples: +### IP Whitelist -Kill transcodes: +Triggers: Playback Start +Conditions: \[ `IP Address` | `is not` | `192.168.0.100 or 192.168.0.101` \] - Set Trigger: Playback Start - Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage {ip_address} is not allowed to access {server_name}. +``` -Kill paused transcodes: +### Kill by platform - Set Trigger: Playback Paused - Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] +Triggers: Playback Start +Conditions: \[ `Platform` | `is` | `Roku or Android` \] -Limit User stream count, kill last stream: +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage {platform} is not allowed on {server_name}. +``` - Set Trigger: Playback Start - Set Conditions: [ {User Streams} | {is greater than} | {3} ] +### Kill transcode by library -IP Whitelist: +Triggers: Playback Start +Conditions: +* \[ `Transcode Decision` | `is` | `transcode` \] +* \[ `Library Name` | `is` | `4K Movies` \] - Set Trigger: Playback Start - Set Conditions: [ {IP Address} | {is not} | {192.168.0.100 or 192.168.0.101} ] +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed from the 4K Movies library. +``` -Kill by platform: +### Kill transcode by original resolution - Set Trigger: Playback Start - Set Conditions: [ {Platform} | {is} | {Roku or Android} ] +Triggers: Playback Start +Conditions: +* \[ `Transcode Decision` | `is` | `transcode` \] +* \[ `Video Resolution` | `is` | `1080 or 720`\] -Kill transcode by library: +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed for {stream_video_resolution}p streams. +``` - Set Trigger: Playback Start - Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] - [ {Library Name} | {is} | {4K Movies} ] +### Kill transcode by bitrate -Kill transcode by original resolution: +Triggers: Playback Start +Conditions: +* \[ `Transcode Decision` | `is` | `transcode` \] +* \[ `Bitrate` | `is greater than` | `4000` \] - Set Trigger: Playback Start - Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] - [ {Video Resolution} | {is} | {1080 or 720}] +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed from over 4 Mbps (Yours: {stream_bitrate}). +``` -Kill transcode by bitrate: +### Kill by hours of the day - Set Trigger: Playback Start - Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] - [ {Bitrate} | {is greater than} | {4000} ] +Kills any streams during 9 AM to 10 AM -Kill by hours of the day: +Triggers: Playback Start +Conditions: \[ `Timestamp` | `begins with` | `09 or 10` \] +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage {server_name} is unavailable between 9 and 10 AM. +``` - Set Trigger: Playback Start - Set Conditions: [ {Timestamp} | {begins with} | {09 or 10} ] - # Killing any streams from 9 AM to 11 AM +### Kill non local streams -Kill non local streams: +Triggers: Playback Start +Conditions: \[ `Stream Local` | `is not` | `1` \] +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --killMessage {server_name} only allows local streams. +``` - Set Trigger: Playback Start - Set Conditions: [ {Stream location} | {is not} | {lan} ] +### Kill transcodes and send a notification to agent 1 + +Triggers: Playback Start +Conditions: \[ `Transcode Decision` | `is` | `transcode` \] + +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} --notify 1 --killMessage Transcoding streams are not allowed. +``` + +### Kill transcodes using the default message + +Triggers: Playback Start +Conditions: \[ `Transcode Decision` | `is` | `transcode` \] + +Arguments: +``` +--jbop stream --username {username} --sessionId {session_id} +``` + +### Kill all of a user's streams with notification + +Triggers: Playback Start +Conditions: \[ `Username` | `is` | `Bob` \] + +Arguments: +``` +--jbop allStreams --userId {user_id} --notify 1 --killMessage Hey Bob, we need to talk! +``` From 75b4fa3229ca7163ec4d1336a500594577aad719 Mon Sep 17 00:00:00 2001 From: Landon Abney Date: Mon, 18 Jun 2018 13:30:41 -0700 Subject: [PATCH 20/20] Add examples for paused streams Add a few examples for usage of the new paused stream functionality. --- killstream/kill_stream.py | 19 ------------------- killstream/readme.md | 27 ++++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/killstream/kill_stream.py b/killstream/kill_stream.py index 480e882..9a26e3f 100644 --- a/killstream/kill_stream.py +++ b/killstream/kill_stream.py @@ -39,25 +39,6 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments: Save Close - -Examples: - -Kill all WAN streams paused longer than 20 minutes, checking every 30 seconds -Script Timeout: 0 -Triggers: Playback Pause -Conditions: Stream Location is not LAN -Arguments (Playback Start): --jbop paused --sessionId {session_id} - --limit 1200 --interval 30 - --killMessage Your stream was paused for over 20 minutes and has been - automatically stopped for you. - -Notes: -* Any of these can have "--notify X" added to them, where X is the -ID shown in Tautulli for a Notification agent, if specified it will send a -message there when a stream is terminated. -* Arguments should all be on one line in Tautulli, they are split here for -easier reading. - """ import requests diff --git a/killstream/readme.md b/killstream/readme.md index a428705..5bf0465 100644 --- a/killstream/readme.md +++ b/killstream/readme.md @@ -14,6 +14,31 @@ Arguments: --jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed. ``` +### Kill non-local streams paused for a long time + +_The default values will kill anything paused for over 20 minutes, checking every 30 seconds._ + +Script Timeout: 0 _**Important!**_ +Triggers: Playback Paused +Conditions: \[ `Stream Local` | `is not` | `1` \] + +Arguments: +``` +--jbop paused --sessionId {session_id} --killMessage Your stream was paused for over 20 minutes and has been automatically stopped for you. +``` + +### Kill streams paused for a custom time + +_This is an example of customizing the paused stream monitoring to check every 15 seconds, and kill any stream paused for over 5 minutes._ + +Script Timeout: 0 _**Important!**_ +Triggers: Playback Paused + +Arguments: +``` +--jbop paused --interval 15 --limit 300 --sessionId {session_id} --killMessage Your stream was paused for over 5 minutes and has been automatically stopped for you. +``` + ### Kill paused transcodes Triggers: Playback Paused @@ -92,7 +117,7 @@ Arguments: ### Kill by hours of the day -Kills any streams during 9 AM to 10 AM +_Kills any streams during 9 AM to 10 AM._ Triggers: Playback Start Conditions: \[ `Timestamp` | `begins with` | `09 or 10` \]