316 lines
10 KiB
Python
316 lines
10 KiB
Python
"""
|
|
Description: Use conditions to kill a stream
|
|
Author: Blacktwin
|
|
Requires: requests
|
|
|
|
Adding the script to Tautulli:
|
|
Taultulli > Settings > Notification Agents > Add a new notification agent >
|
|
Script
|
|
|
|
Configuration:
|
|
Taultulli > Settings > Notification Agents > New Script > Configuration:
|
|
|
|
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: Playback Start and/or Playback Pause
|
|
Save
|
|
|
|
Conditions:
|
|
Taultulli > Settings > Notification Agents > New Script > Conditions:
|
|
|
|
Set Conditions: [{condition} | {operator} | {value} ]
|
|
Save
|
|
|
|
Script Arguments:
|
|
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
|
|
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
|
|
import argparse
|
|
import sys
|
|
import os
|
|
from time import sleep
|
|
from datetime import datetime
|
|
|
|
TAUTULLI_URL = ''
|
|
TAUTULLI_APIKEY = ''
|
|
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 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'
|
|
# 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', 'paused']
|
|
|
|
|
|
def send_notification(subject_text, body_text, notifier_id):
|
|
"""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,
|
|
'subject': subject_text,
|
|
'body': body_text}
|
|
|
|
try:
|
|
r = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
|
response = r.json()
|
|
|
|
if response['response']['result'] == 'success':
|
|
sys.stdout.write("Successfully sent Tautulli notification.")
|
|
else:
|
|
raise Exception(response['response']['message'])
|
|
except Exception as e:
|
|
sys.stderr.write(
|
|
"Tautulli API 'notify' request failed: {0}.".format(e))
|
|
return None
|
|
|
|
|
|
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 = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
|
response = req.json()
|
|
|
|
res_data = response['response']['data']['sessions']
|
|
return res_data
|
|
|
|
except Exception as e:
|
|
sys.stderr.write(
|
|
"Tautulli API 'get_activity' request failed: {0}.".format(e))
|
|
pass
|
|
|
|
|
|
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,
|
|
'message': message}
|
|
|
|
try:
|
|
req = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
|
response = req.json()
|
|
|
|
if response['response']['result'] == 'success':
|
|
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))
|
|
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()
|
|
fudge_factor = 100 # Keep checking this long after the defined limit
|
|
paused_time = 0
|
|
check_limit = limit + interval + fudge_factor
|
|
|
|
while paused_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']
|
|
|
|
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 found_session:
|
|
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.")
|
|
parser.add_argument('--jbop', required=True, choices=SELECTOR,
|
|
help='Kill selector.\nChoices: (%(choices)s)')
|
|
parser.add_argument('--userId', type=int,
|
|
help='The unique identifier for the user.')
|
|
parser.add_argument('--username',
|
|
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.')
|
|
|
|
opts = parser.parse_args()
|
|
|
|
if opts.killMessage:
|
|
message = ' '.join(opts.killMessage)
|
|
else:
|
|
message = ''
|
|
|
|
if opts.jbop == 'stream':
|
|
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, opts.notify, opts.username)
|
|
elif opts.jbop == 'paused':
|
|
terminate_long_pause(opts.sessionId, message, opts.limit,
|
|
opts.interval, opts.notify)
|