368 lines
12 KiB
Python
368 lines
12 KiB
Python
"""
|
|
Description: Limiting Plex users by plays, watches, or total time from Tautulli.
|
|
Author: Blacktwin, Arcanemagus
|
|
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: ./limiterr.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
|
|
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 --username {username}
|
|
--sessionId {session_id} --notify notifierID
|
|
--grandparent_rating_key {grandparent_rating_key}
|
|
--limit plays=3 --delay 60
|
|
--killMessage 'Your message here.'
|
|
|
|
Save
|
|
Close
|
|
"""
|
|
|
|
import requests
|
|
import argparse
|
|
from datetime import datetime
|
|
import sys
|
|
import os
|
|
from plexapi.server import PlexServer, CONFIG
|
|
from time import time as ttime
|
|
|
|
TAUTULLI_URL = ''
|
|
TAUTULLI_APIKEY = ''
|
|
PLEX_URL = ''
|
|
PLEX_TOKEN = ''
|
|
|
|
# Environmental Variables
|
|
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
|
|
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN)
|
|
TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL)
|
|
TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
|
|
TAUTULLI_ENCODING = os.getenv('TAUTULLI_ENCODING', 'UTF-8')
|
|
|
|
# Using CONFIG file
|
|
# PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
|
# PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
|
# TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl', TAUTULLI_URL)
|
|
# TAUTULLI_APIKEY = CONFIG.data['auth'].get('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}."
|
|
LIMIT_MESSAGE = 'Are you still watching or are you asleep? ' \
|
|
'If not please wait ~{delay} seconds and try again.'
|
|
|
|
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)
|
|
|
|
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
|
lib_dict = {x.title : x.key for x in plex.library.sections()}
|
|
|
|
|
|
SELECTOR = ['watch', 'plays', 'time', 'limit']
|
|
TODAY = datetime.today().strftime('%Y-%m-%d')
|
|
unix_time = int(ttime())
|
|
|
|
|
|
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:
|
|
req = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
|
response = req.json()
|
|
|
|
if response['response']['result'] == 'success':
|
|
sys.stdout.write("Successfully sent Tautulli notification.\n")
|
|
else:
|
|
raise Exception(response['response']['message'])
|
|
except Exception as e:
|
|
sys.stderr.write(
|
|
"Tautulli API 'notify' request failed: {0}.\n".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}.\n".format(e))
|
|
pass
|
|
|
|
|
|
def get_history(username, start_date=None, section_id=None):
|
|
"""Get the Tautulli history.
|
|
|
|
Parameters
|
|
----------
|
|
username : str
|
|
The username to gather history from.
|
|
|
|
Optional
|
|
----------
|
|
start_date : str "YYYY-MM-DD"
|
|
The date in history to search.
|
|
section_id : int
|
|
The libraries numeric identifier
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
The total number of watches, plays, or total playtime.
|
|
"""
|
|
payload = {'apikey': TAUTULLI_APIKEY,
|
|
'cmd': 'get_history',
|
|
'user': username}
|
|
|
|
if start_date:
|
|
payload['start_date'] = TODAY
|
|
|
|
if section_id:
|
|
payload['section_id '] = section_id
|
|
|
|
try:
|
|
req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
|
response = req.json()
|
|
|
|
res_data = response['response']['data']
|
|
return res_data
|
|
|
|
except Exception as e:
|
|
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
|
|
|
|
|
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()
|
|
|
|
print(response)
|
|
if response['response']['result'] == 'success':
|
|
sys.stdout.write(
|
|
"Successfully killed Plex session: {0}.\n".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 arg_decoding(arg):
|
|
return arg.decode(TAUTULLI_ENCODING).encode('UTF-8')
|
|
|
|
|
|
def unshare(user):
|
|
print('{user} has reached their limit. Unsharing...'.format(user=user))
|
|
plex.myPlexAccount().updateFriend(user=user, server=plex, removeSections=True, sections='2')
|
|
print('Unshared all libraries from {user}.'.format(user=user))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="Limiting Plex users by plays, watches, or total time from Tautulli.")
|
|
parser.add_argument('--jbop', required=True, choices=SELECTOR,
|
|
help='Limit selector.\nChoices: (%(choices)s)')
|
|
parser.add_argument('--username', required=True,
|
|
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', action='append', type=lambda kv: kv.split("="),
|
|
help='The limit related to the limit selector chosen.')
|
|
parser.add_argument('--grandparent_rating_key', type=int,
|
|
help='The unique identifier for the TV show or artist.')
|
|
parser.add_argument('--delay', type=int, default=60,
|
|
help='The seconds to wait in order to deem user is active.')
|
|
parser.add_argument('--killMessage', nargs='+',
|
|
help='Message to send to user whose stream is killed.')
|
|
parser.add_argument('--section', default=False, choices=lib_dict.keys(), metavar='',
|
|
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
|
'(choices: %(choices)s)')
|
|
|
|
opts = parser.parse_args()
|
|
|
|
total_limit = 0
|
|
total_jbop = 0
|
|
|
|
if opts.limit:
|
|
limit = dict(opts.limit)
|
|
|
|
for key, value in limit.items():
|
|
if key == 'days':
|
|
total_limit += int(value) * (24 * 60 * 60)
|
|
elif key == 'hours':
|
|
total_limit += int(value) * (60 * 60)
|
|
elif key == 'minutes':
|
|
total_limit += int(value) * 60
|
|
elif key == 'plays':
|
|
total_limit = int(value)
|
|
|
|
if not opts.sessionId:
|
|
sys.stderr.write("No sessionId provided! Is this synced content?\n")
|
|
sys.exit(1)
|
|
|
|
if opts.killMessage:
|
|
message = ' '.join(opts.killMessage)
|
|
else:
|
|
message = ''
|
|
|
|
if opts.section:
|
|
section_id = lib_dict[opts.section]
|
|
history = get_history(username=opts.username, section_id=section_id)
|
|
else:
|
|
history = get_history(username=opts.username)
|
|
|
|
if opts.jbop == 'watch':
|
|
total_jbop = sum([data['watched_status'] for data in history['data']])
|
|
if opts.jbop == 'time':
|
|
total_jbop = sum([data['duration'] for data in history['data']])
|
|
if opts.jbop == 'plays':
|
|
total_jbop = history['recordsFiltered']
|
|
|
|
if total_jbop:
|
|
if total_jbop > total_limit:
|
|
print('Total {} ({}) is greater than limit ({}).'
|
|
.format(opts.jbop, total_jbop, total_limit))
|
|
terminate_session(opts.sessionId, message, opts.notify, opts.username)
|
|
else:
|
|
print('Total {} ({}) is less than limit ({}).'
|
|
.format(opts.jbop, total_jbop, total_limit))
|
|
|
|
if opts.jbop == 'limit' and opts.grandparent_rating_key:
|
|
history = get_history(username=opts.username, start_date=True)
|
|
message = LIMIT_MESSAGE.format(delay=opts.delay)
|
|
ep_watched = [data['watched_status'] for data in history['data']
|
|
if data['grandparent_rating_key'] == opts.grandparent_rating_key
|
|
and data['watched_status'] == 1]
|
|
if not ep_watched:
|
|
ep_watched = 0
|
|
else:
|
|
ep_watched = sum(ep_watched)
|
|
|
|
stopped_time = [data['stopped'] for data in history['data']
|
|
if data['grandparent_rating_key'] == opts.grandparent_rating_key
|
|
and data['watched_status'] == 1]
|
|
if not stopped_time:
|
|
stopped_time = unix_time
|
|
else:
|
|
stopped_time = stopped_time[0]
|
|
|
|
if abs(stopped_time - unix_time) > opts.delay:
|
|
print('{} is awake!'.format(opts.username))
|
|
sys.exit(1)
|
|
|
|
if ep_watched >= total_limit:
|
|
print("{}'s limit is {} and has watched {} episodes of this show today."
|
|
.format(opts.username, total_limit, ep_watched))
|
|
terminate_session(opts.sessionId, message, opts.notify, opts.username)
|
|
else:
|
|
print("{}'s limit is {} but has only watched {} episodes of this show today."
|
|
.format(opts.username, total_limit, ep_watched)) |