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
Author: Blacktwin
Author: Blacktwin, Arcanemagus, samwiseg00
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:
@ -31,17 +33,20 @@ 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} --notify notifierID
--interval 30 --limit 1200
--killMessage Your message here. No quotes.
Save
Close
"""
import requests
import argparse
import sys
import os
from time import sleep
from datetime import datetime
TAUTULLI_URL = ''
TAUTULLI_APIKEY = ''
@ -49,19 +54,35 @@ 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
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.
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):
# 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,
@ -69,7 +90,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':
@ -77,30 +98,69 @@ 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
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'}
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']
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("Tautulli API 'get_activity' request failed: {0}.".format(e))
sys.stderr.write(
"Tautulli API 'get_activity' request failed: {0}.".format(e))
pass
def terminate_session(session_id, message):
# Stop a streaming session.
def get_user_session_ids(user_id):
"""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,
'cmd': 'terminate_session',
'session_id': session_id,
@ -111,16 +171,79 @@ 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))
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:
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
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__":
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,
@ -129,10 +252,15 @@ 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('--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.')
parser.add_argument('--notify', type=int,
help='Notification Agent ID number to Agent to send notification.')
opts = parser.parse_args()
@ -142,12 +270,11 @@ 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_activity(opts.userId)
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)
elif opts.jbop == 'paused':
terminate_long_pause(opts.sessionId, message, opts.limit,
opts.interval, 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
--jbop stream --userId {user_id} --username {username} --sessionId {session_id} --killMessage You did something wrong. --notify 1
### Kill non-local streams paused for a long time
Kill all the offending users streams with a custom message and send notification to notfication agent ID 1
--jbop allStreams --userId {user_id} --username {username} --sessionId {session_id} --killMessage You did something wrong. --notify 1
_The default values will kill anything paused for over 20 minutes, checking every 30 seconds._
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
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
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:
Set Trigger: Playback Paused
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
### Kill paused transcodes
Limit User stream count, kill last stream:
Set Trigger: Playback Start
Set Conditions: [ {User Streams} | {is greater than} | {3} ]
Triggers: Playback Paused
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
IP Whitelist:
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 Paused streams are automatically stopped.
```
Kill by platform:
Set Trigger: Playback Start
Set Conditions: [ {Platform} | {is} | {Roku or Android} ]
### Limit User stream count, kill last stream
Kill transcode by library:
Set Trigger: Playback Start
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
[ {Library Name} | {is} | {4K Movies} ]
Triggers: Playback Start
Conditions: \[ `User Streams` | `is greater than` | `3` \]
Kill transcode by original resolution:
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 You are only allowed 3 streams.
```
Kill transcode by bitrate:
Set Trigger: Playback Start
Set Conditions: [ {Transcode Decision} | {is} | {transcode} ]
[ {Bitrate} | {is greater than} | {4000} ]
### IP Whitelist
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
Triggers: Playback Start
Conditions: \[ `IP Address` | `is not` | `192.168.0.100 or 192.168.0.101` \]
Kill non local streams:
Set Trigger: Playback Start
Set Conditions: [ {Stream location} | {is} | {wan} ]
or
Set Conditions: [ {Stream location} | {is not} | {lan} ]
Arguments:
```
--jbop stream --username {username} --sessionId {session_id} --killMessage {ip_address} is not allowed to access {server_name}.
```
### 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)