Merge pull request #76 from Arcanemagus/kill_stream-paused

Incorporate waiting to kill paused streams into kill_stream.py
This commit is contained in:
blacktwin 2018-06-18 21:19:30 -04:00 committed by GitHub
commit 203a3b0bb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 305 additions and 212 deletions

View File

@ -1,23 +1,25 @@
""" """
Description: Use conditions to kill a stream Description: Use conditions to kill a stream
Author: Blacktwin Author: Blacktwin, Arcanemagus, samwiseg00
Requires: requests Requires: requests
Enabling Scripts in Tautulli: Adding the script to Tautulli:
Taultulli > Settings > Notification Agents > Add a Notification Agent > Script Taultulli > Settings > Notification Agents > Add a new notification agent >
Script
Configuration: Configuration:
Taultulli > Settings > Notification Agents > New Script > Configuration: Taultulli > Settings > Notification Agents > New Script > Configuration:
Script Name: kill_stream Script Folder: /path/to/your/scripts
Set Script Timeout: {timeout} Script File: ./kill_stream.py (Should be selectable in a dropdown list)
Script Timeout: {timeout}
Description: Kill stream(s) Description: Kill stream(s)
Save Save
Triggers: Triggers:
Taultulli > Settings > Notification Agents > New Script > Triggers: Taultulli > Settings > Notification Agents > New Script > Triggers:
Check: {trigger} Check: Playback Start and/or Playback Pause
Save Save
Conditions: Conditions:
@ -31,17 +33,20 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
Select: Playback Start, Playback Pause Select: Playback Start, Playback Pause
Arguments: --jbop SELECTOR --userId {user_id} --username {username} Arguments: --jbop SELECTOR --userId {user_id} --username {username}
--sessionId {session_id} --killMessage Your message here. No quotes. --notify notifierID --sessionId {session_id} --notify notifierID
--interval 30 --limit 1200
--killMessage Your message here. No quotes.
Save Save
Close Close
""" """
import requests import requests
import argparse import argparse
import sys import sys
import os import os
from time import sleep
from datetime import datetime
TAUTULLI_URL = '' TAUTULLI_URL = ''
TAUTULLI_APIKEY = '' TAUTULLI_APIKEY = ''
@ -49,19 +54,35 @@ TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL)
TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY) TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
SUBJECT_TEXT = "Tautulli has killed a stream." 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() sess = requests.Session()
# Ignore verifying the SSL certificate # 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, # 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.
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'] SELECTOR = ['stream', 'allStreams', 'paused']
def send_notification(subject_text, body_text, notifier_id): 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, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'notify', 'cmd': 'notify',
'notifier_id': notifier_id, 'notifier_id': notifier_id,
@ -69,7 +90,7 @@ def send_notification(subject_text, body_text, notifier_id):
'body': body_text} 'body': body_text}
try: try:
r = requests.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) r = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json() response = r.json()
if response['response']['result'] == 'success': if response['response']['result'] == 'success':
@ -77,30 +98,69 @@ def send_notification(subject_text, body_text, notifier_id):
else: else:
raise Exception(response['response']['message']) raise Exception(response['response']['message'])
except Exception as e: 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 return None
def get_activity(user_id): def get_activity():
# Get the current activity on the PMS. """Get the current activity on the PMS.
Returns
-------
list
The current active sessions on the Plex server.
"""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_activity'} 'cmd': 'get_activity'}
try: try:
req = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = req.json() response = req.json()
res_data = response['response']['data']['sessions'] res_data = response['response']['data']['sessions']
user_streams = [d['session_id'] for d in res_data if d['user_id'] == user_id] return res_data
return user_streams
except Exception as e: 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 pass
def terminate_session(session_id, message): def get_user_session_ids(user_id):
# Stop a streaming session. """Get current session IDs for a specific user.
Parameters
----------
user_id : int
The ID of the user to grab sessions for.
Returns
-------
list
The active session IDs 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, notifier=None, username=None):
"""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, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'terminate_session', 'cmd': 'terminate_session',
'session_id': session_id, 'session_id': session_id,
@ -111,16 +171,79 @@ def terminate_session(session_id, message):
response = req.json() response = req.json()
if response['response']['result'] == 'success': 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))
if notifier:
if username:
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)
else: else:
raise Exception(response['response']['message']) raise Exception(response['response']['message'])
except Exception as e: 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 return None
def terminate_long_pause(session_id, message, limit, interval, notify=None):
"""Kills the session if it is paused for longer than <limit> 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()
checked_time = 0
# Continue checking 2 intervals past the allowed limit in order to
# account for system variances.
check_limit = limit + (interval * 2)
while checked_time < check_limit:
sessions = get_activity()
found_session = False
for session in sessions:
if session['session_id'] == session_id:
found_session = True
state = session['state']
now = datetime.now()
checked_time = (now - start).total_seconds()
if state == 'paused':
if checked_time >= limit:
terminate_session(session_id, message, notify)
return
else:
sleep(interval)
elif state == 'playing' or state == 'buffering':
sys.stdout.write(
"Session '{}' has resumed, ".format(session_id) +
"stopping monitoring.")
return
if not found_session:
sys.stdout.write(
"Session '{}' is no longer active ".format(session_id) +
"on the server, stopping monitoring.")
return
if __name__ == "__main__": 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, parser.add_argument('--jbop', required=True, choices=SELECTOR,
help='Kill selector.\nChoices: (%(choices)s)') help='Kill selector.\nChoices: (%(choices)s)')
parser.add_argument('--userId', type=int, parser.add_argument('--userId', type=int,
@ -129,10 +252,15 @@ if __name__ == "__main__":
help='The username of the person streaming.') help='The username of the person streaming.')
parser.add_argument('--sessionId', required=True, parser.add_argument('--sessionId', required=True,
help='The unique identifier for the stream.') help='The unique identifier for the stream.')
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='+', parser.add_argument('--killMessage', nargs='+',
help='Message to send to user whose stream is killed.') 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.')
opts = parser.parse_args() opts = parser.parse_args()
@ -142,12 +270,11 @@ if __name__ == "__main__":
message = '' message = ''
if opts.jbop == 'stream': if opts.jbop == 'stream':
terminate_session(opts.sessionId, message) terminate_session(opts.sessionId, message, opts.notify, opts.username)
elif opts.jbop == 'allStreams': elif opts.jbop == 'allStreams':
streams = get_activity(opts.userId) streams = get_user_session_ids(opts.userId)
for session_id in streams: for session_id in streams:
terminate_session(session_id, message) terminate_session(session_id, message, opts.notify, opts.username)
elif opts.jbop == 'paused':
if opts.notify: terminate_long_pause(opts.sessionId, message, opts.limit,
BODY_TEXT = BODY_TEXT.format(user=opts.username, message=message) opts.interval, opts.notify)
send_notification(SUBJECT_TEXT, BODY_TEXT, opts.notify)

View File

@ -1,81 +1,166 @@
## 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 transcodes
### Kill_stream.py examples: Triggers: Playback Start
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
#### Arguments examples: Arguments:
```
--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed.
```
Kill the one offending stream with a custom message and send notification to notfication agent ID 1 ### Kill non-local streams paused for a long time
--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 _The default values will kill anything paused for over 20 minutes, checking every 30 seconds._
--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 Script Timeout: 0 _**Important!**_
Triggers: Playback Paused
Conditions: \[ `Stream Local` | `is not` | `1` \]
--jbop stream --userId {user_id} --username {username} --sessionId {session_id} 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
#### Condition Examples: _This is an example of customizing the paused stream monitoring to check every 15 seconds, and kill any stream paused for over 5 minutes._
Kill transcodes: Script Timeout: 0 _**Important!**_
Triggers: Playback Paused
Set Trigger: Playback Start Arguments:
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] ```
--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: ### Kill paused transcodes
Set Trigger: Playback Paused
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
Limit User stream count, kill last stream: Triggers: Playback Paused
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
Set Trigger: Playback Start
Set Conditions: [ {User Streams} | {is greater than} | {3} ]
IP Whitelist: Arguments:
```
Set Trigger: Playback Start --jbop stream --username {username} --sessionId {session_id} --killMessage Paused streams are automatically stopped.
Set Conditions: [ {IP Address} | {is not} | {192.168.0.100 or 192.168.0.101} ] ```
Kill by platform: ### Limit User stream count, kill last stream
Set Trigger: Playback Start
Set Conditions: [ {Platform} | {is} | {Roku or Android} ]
Kill transcode by library: Triggers: Playback Start
Conditions: \[ `User Streams` | `is greater than` | `3` \]
Set Trigger: Playback Start
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
[ {Library Name} | {is} | {4K Movies} ]
Kill transcode by original resolution: Arguments:
```
Set Trigger: Playback Start --jbop stream --username {username} --sessionId {session_id} --killMessage You are only allowed 3 streams.
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ] ```
[ {Video Resolution} | {is} | {1080 or 720}]
Kill transcode by bitrate: ### IP Whitelist
Set Trigger: Playback Start
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
[ {Bitrate} | {is greater than} | {4000} ]
Kill by hours of the day: Triggers: Playback Start
Conditions: \[ `IP Address` | `is not` | `192.168.0.100 or 192.168.0.101` \]
Set Trigger: Playback Start
Set Conditions: [ {Timestamp} | {begins with} | {09 or 10} ]
# Killing any streams from 9am to 11am
Kill non local streams: Arguments:
```
Set Trigger: Playback Start --jbop stream --username {username} --sessionId {session_id} --killMessage {ip_address} is not allowed to access {server_name}.
Set Conditions: [ {Stream location} | {is} | {wan} ] ```
or
Set Conditions: [ {Stream location} | {is not} | {lan} ]
### Kill by platform
Triggers: Playback Start
Conditions: \[ `Platform` | `is` | `Roku or Android` \]
Arguments:
```
--jbop stream --username {username} --sessionId {session_id} --killMessage {platform} is not allowed on {server_name}.
```
### Kill transcode by library
Triggers: Playback Start
Conditions:
* \[ `Transcode Decision` | `is` | `transcode` \]
* \[ `Library Name` | `is` | `4K Movies` \]
Arguments:
```
--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed from the 4K Movies library.
```
### Kill transcode by original resolution
Triggers: Playback Start
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 for {stream_video_resolution}p streams.
```
### Kill transcode by bitrate
Triggers: Playback Start
Conditions:
* \[ `Transcode Decision` | `is` | `transcode` \]
* \[ `Bitrate` | `is greater than` | `4000` \]
Arguments:
```
--jbop stream --username {username} --sessionId {session_id} --killMessage Transcoding streams are not allowed from over 4 Mbps (Yours: {stream_bitrate}).
```
### Kill by hours of the day
_Kills any streams during 9 AM to 10 AM._
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.
```
### 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.
```
### 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!
```

View File

@ -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)