commit
472e438967
@ -1,13 +1,13 @@
|
||||
"""
|
||||
Description: Use conditions to kill a stream
|
||||
Author: Blacktwin, Arcanemagus, Samwiseg00, JonnyWong16
|
||||
Author: Blacktwin, Arcanemagus, Samwiseg0, JonnyWong16, DirtyCajunRice
|
||||
|
||||
Adding the script to Tautulli:
|
||||
Taultulli > Settings > Notification Agents > Add a new notification agent >
|
||||
Tautulli > Settings > Notification Agents > Add a new notification agent >
|
||||
Script
|
||||
|
||||
Configuration:
|
||||
Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Tautulli > Settings > Notification Agents > New Script > Configuration:
|
||||
|
||||
Script Folder: /path/to/your/scripts
|
||||
Script File: ./kill_stream.py (Should be selectable in a dropdown list)
|
||||
@ -16,19 +16,19 @@ Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Save
|
||||
|
||||
Triggers:
|
||||
Taultulli > Settings > Notification Agents > New Script > Triggers:
|
||||
Tautulli > Settings > Notification Agents > New Script > Triggers:
|
||||
|
||||
Check: Playback Start and/or Playback Pause
|
||||
Save
|
||||
|
||||
Conditions:
|
||||
Taultulli > Settings > Notification Agents > New Script > Conditions:
|
||||
Tautulli > Settings > Notification Agents > New Script > Conditions:
|
||||
|
||||
Set Conditions: [{condition} | {operator} | {value} ]
|
||||
Save
|
||||
|
||||
Script Arguments:
|
||||
Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
Tautulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
Select: Playback Start, Playback Pause
|
||||
Arguments: --jbop SELECTOR --userId {user_id} --username {username}
|
||||
@ -46,11 +46,13 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import requests
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
@ -109,13 +111,15 @@ def debug_dump_vars():
|
||||
.rjust(len(TAUTULLI_APIKEY), "x"))
|
||||
|
||||
|
||||
def get_all_streams(user_id=None):
|
||||
def get_all_streams(tautulli, user_id=None):
|
||||
"""Get a list of all current streams.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_id : int
|
||||
The ID of the user to grab sessions for.
|
||||
tautulli : obj
|
||||
Tautulli object.
|
||||
Returns
|
||||
-------
|
||||
objects
|
||||
@ -131,17 +135,17 @@ def get_all_streams(user_id=None):
|
||||
return streams
|
||||
|
||||
|
||||
def notify(opts, message, kill_type=None, stream=None):
|
||||
def notify(all_opts, message, kill_type=None, stream=None, tautulli=None):
|
||||
"""Decides which notifier type to use"""
|
||||
if opts.notify and opts.richMessage:
|
||||
rich_notify(opts.notify, opts.richMessage, opts.richColor, kill_type,
|
||||
opts.serverName, opts.plexUrl, opts.posterUrl, message, stream)
|
||||
elif opts.notify:
|
||||
basic_notify(opts.notify, opts.sessionId, opts.username, message)
|
||||
if all_opts.notify and all_opts.richMessage:
|
||||
rich_notify(all_opts.notify, all_opts.richMessage, all_opts.richColor, kill_type,
|
||||
all_opts.serverName, all_opts.plexUrl, all_opts.posterUrl, message, stream, tautulli)
|
||||
elif all_opts.notify:
|
||||
basic_notify(all_opts.notify, all_opts.sessionId, all_opts.username, message, stream, tautulli)
|
||||
|
||||
|
||||
def rich_notify(notifier_id, rich_type, color=None, kill_type=None, server_name=None,
|
||||
plex_url=None, poster_url=None, message=None, stream=None):
|
||||
plex_url=None, poster_url=None, message=None, stream=None, tautulli=None):
|
||||
"""Decides which rich notifier type to use. Set default values for empty variables
|
||||
|
||||
Parameters
|
||||
@ -150,6 +154,8 @@ def rich_notify(notifier_id, rich_type, color=None, kill_type=None, server_name=
|
||||
The ID of the user to grab sessions for.
|
||||
rich_type : str
|
||||
Contains 'discord' or 'slack'.
|
||||
color : Union[int, str]
|
||||
Hex string or integer representation of color.
|
||||
kill_type : str
|
||||
The kill type used.
|
||||
server_name : str
|
||||
@ -162,22 +168,26 @@ def rich_notify(notifier_id, rich_type, color=None, kill_type=None, server_name=
|
||||
Message sent to the client.
|
||||
stream : obj
|
||||
Stream object.
|
||||
tautulli : obj
|
||||
Tautulli object.
|
||||
"""
|
||||
notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream)
|
||||
|
||||
# Initialize Variables
|
||||
title = ''
|
||||
footer = ''
|
||||
# Set a default server_name if none is provided
|
||||
if server_name is None:
|
||||
server_name = 'Plex Server'
|
||||
|
||||
# Set a defult color if none is provided
|
||||
# Set a default color if none is provided
|
||||
if color is None:
|
||||
color = '#E5A00D'
|
||||
|
||||
# Set a defult plexUrl if none is provided
|
||||
# Set a default plexUrl if none is provided
|
||||
if plex_url is None:
|
||||
plex_url = 'https://app.plex.tv'
|
||||
|
||||
# Set a defult posterUrl if none is provided
|
||||
# Set a default posterUrl if none is provided
|
||||
if poster_url is None:
|
||||
poster_url = TAUTULLI_ICON
|
||||
|
||||
@ -207,7 +217,7 @@ def rich_notify(notifier_id, rich_type, color=None, kill_type=None, server_name=
|
||||
notification.send_slack(title, color, poster_url, plex_url, message, footer)
|
||||
|
||||
|
||||
def basic_notify(notifier_id, session_id, username=None, message=None):
|
||||
def basic_notify(notifier_id, session_id, username=None, message=None, stream=None, tautulli=None):
|
||||
"""Basic notifier"""
|
||||
notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream)
|
||||
|
||||
@ -225,11 +235,11 @@ class Tautulli:
|
||||
self.apikey = apikey
|
||||
self.debug = debug
|
||||
|
||||
self.session = requests.Session()
|
||||
self.adapters = requests.adapters.HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session = Session()
|
||||
self.adapters = HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session.mount('http://', self.adapters)
|
||||
self.session.mount('https://', self.adapters)
|
||||
|
||||
@ -240,14 +250,14 @@ class Tautulli:
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
def _call_api(self, cmd, payload, method='GET', debug=None):
|
||||
def _call_api(self, cmd, payload, method='GET'):
|
||||
payload['cmd'] = cmd
|
||||
payload['apikey'] = self.apikey
|
||||
|
||||
try:
|
||||
response = self.session.request(method, self.url + '/api/v2', params=payload)
|
||||
except:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL?".format(cmd))
|
||||
except RequestException as e:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
|
||||
if self.debug:
|
||||
traceback.print_exc()
|
||||
return
|
||||
@ -303,11 +313,12 @@ class Tautulli:
|
||||
|
||||
class Stream:
|
||||
def __init__(self, session_id=None, user_id=None, username=None, tautulli=None, session=None):
|
||||
self.state = None
|
||||
self.ip_address = None
|
||||
self.session_id = session_id
|
||||
self.user_id = user_id
|
||||
self.username = username
|
||||
self.session_exists = False
|
||||
|
||||
self.tautulli = tautulli
|
||||
|
||||
if session is not None:
|
||||
@ -579,38 +590,38 @@ if __name__ == "__main__":
|
||||
sys.exit(1)
|
||||
|
||||
if opts.debug:
|
||||
# Import traceback to get more deatiled information
|
||||
# Import traceback to get more detailed information
|
||||
import traceback
|
||||
# Dump the ENVs passed from tatutulli
|
||||
# Dump the ENVs passed from tautulli
|
||||
debug_dump_vars()
|
||||
|
||||
# Create a Tautulli instance
|
||||
tautulli = Tautulli(TAUTULLI_URL.rstrip('/'), TAUTULLI_APIKEY, VERIFY_SSL, opts.debug)
|
||||
tautulli_server = Tautulli(TAUTULLI_URL.rstrip('/'), TAUTULLI_APIKEY, VERIFY_SSL, opts.debug)
|
||||
|
||||
# Create initial Stream object with basic info
|
||||
stream = Stream(opts.sessionId, opts.userId, opts.username, tautulli)
|
||||
tautulli_stream = Stream(opts.sessionId, opts.userId, opts.username, tautulli_server)
|
||||
|
||||
# Only pull all stream info if using richMessage
|
||||
if opts.notify and opts.richMessage:
|
||||
stream.get_all_stream_info()
|
||||
tautulli_stream.get_all_stream_info()
|
||||
|
||||
# Set a defult message if none is provided
|
||||
# Set a default message if none is provided
|
||||
if opts.killMessage:
|
||||
message = ' '.join(opts.killMessage)
|
||||
kill_message = ' '.join(opts.killMessage)
|
||||
else:
|
||||
message = 'The server owner has ended the stream.'
|
||||
kill_message = 'The server owner has ended the stream.'
|
||||
|
||||
if opts.jbop == 'stream':
|
||||
stream.terminate(message)
|
||||
notify(opts, message, 'Stream', stream)
|
||||
tautulli_stream.terminate(kill_message)
|
||||
notify(opts, kill_message, 'Stream', tautulli_stream, tautulli_server)
|
||||
|
||||
elif opts.jbop == 'allStreams':
|
||||
streams = get_all_streams(opts.userId)
|
||||
for stream in streams:
|
||||
tautulli.terminate_session(session_id=stream.session_id, message=message)
|
||||
notify(opts, message, 'All Streams', stream)
|
||||
all_streams = get_all_streams(tautulli_server, opts.userId)
|
||||
for a_stream in all_streams:
|
||||
tautulli_server.terminate_session(session_id=a_stream.session_id, message=kill_message)
|
||||
notify(opts, kill_message, 'All Streams', a_stream, tautulli_server)
|
||||
|
||||
elif opts.jbop == 'paused':
|
||||
killed_stream = stream.terminate_long_pause(message, opts.limit, opts.interval)
|
||||
killed_stream = tautulli_stream.terminate_long_pause(kill_message, opts.limit, opts.interval)
|
||||
if killed_stream:
|
||||
notify(opts, message, 'Paused', stream)
|
||||
notify(opts, kill_message, 'Paused', tautulli_stream, tautulli_server)
|
||||
|
@ -6,7 +6,10 @@ Killing streams is a Plex Pass only feature. So these scripts will **only** work
|
||||
|
||||
### Kill transcodes
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
|
||||
Arguments:
|
||||
@ -59,6 +62,18 @@ Arguments:
|
||||
--jbop stream --username {username} --sessionId {session_id} --killMessage 'You are only allowed 3 streams.'
|
||||
```
|
||||
|
||||
### Limit User streams to one unique IP
|
||||
|
||||
Triggers: Playback Start
|
||||
Settings:
|
||||
* Notifications & Newsletters > Show Advanced > `User Concurrent Streams Notifications by IP Address` | `Checked`
|
||||
* Notifications & Newsletters > `User Concurrent Stream Threshold` | `2`
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop stream --username {username} --sessionId {session_id} --killMessage 'You are only allowed to stream from one location at a time.'
|
||||
```
|
||||
|
||||
### IP Whitelist
|
||||
|
||||
Triggers: Playback Start
|
||||
@ -81,7 +96,10 @@ Arguments:
|
||||
|
||||
### Kill transcode by library
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions:
|
||||
* \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
* \[ `Library Name` | `is` | `4K Movies` \]
|
||||
@ -93,7 +111,10 @@ Arguments:
|
||||
|
||||
### Kill transcode by original resolution
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions:
|
||||
* \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
* \[ `Video Resolution` | `is` | `1080 or 720`\]
|
||||
@ -105,7 +126,10 @@ Arguments:
|
||||
|
||||
### Kill transcode by bitrate
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions:
|
||||
* \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
* \[ `Bitrate` | `is greater than` | `4000` \]
|
||||
@ -137,7 +161,10 @@ Arguments:
|
||||
|
||||
### Kill transcodes and send a notification to agent 1
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
|
||||
Arguments:
|
||||
@ -147,7 +174,10 @@ Arguments:
|
||||
|
||||
### Kill transcodes using the default message
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
|
||||
Arguments:
|
||||
@ -211,3 +241,11 @@ Tautulli > Script Agent > Script > Tautulli > Webhook Agent > Discord/Slack
|
||||
### Debug
|
||||
|
||||
Add `--debug` to enable debug logging.
|
||||
|
||||
### Conditions considerations
|
||||
|
||||
#### Kill transcode variants
|
||||
|
||||
All examples use \[ `Transcode Decision` | `is` | `transcode` \] which will kill any variant of transcoding.
|
||||
If you want to allow audio or container transcoding and only drop video transcodes, your condition would change to
|
||||
\[ `Video Decision` | `is` | `transcode` \]
|
@ -3,4 +3,5 @@
|
||||
# pip install -r requirements.txt
|
||||
#---------------------------------------------------------
|
||||
requests
|
||||
plexapi
|
||||
plexapi
|
||||
urllib3
|
48
utility/enable_disable_all_guest_access.py
Normal file
48
utility/enable_disable_all_guest_access.py
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Description: Enable or disable all users remote access to Tautulli
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, python3.6+
|
||||
"""
|
||||
|
||||
from requests import Session
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
ENABLE_REMOTE_ACCESS = True
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_API_KEY = ''
|
||||
|
||||
# Do not edit past this line #
|
||||
session = Session()
|
||||
session.params = {'apikey': TAUTULLI_API_KEY}
|
||||
formatted_url = f'{TAUTULLI_URL}/api/v2'
|
||||
|
||||
request = session.get(formatted_url, params={'cmd': 'get_users'})
|
||||
|
||||
tautulli_users = None
|
||||
try:
|
||||
tautulli_users = request.json()['response']['data']
|
||||
except JSONDecodeError:
|
||||
exit("Error talking to Tautulli API, please check your TAUTULLI_URL")
|
||||
|
||||
allow_guest = 1 if ENABLE_REMOTE_ACCESS else 0
|
||||
string_representation = 'Enabled' if ENABLE_REMOTE_ACCESS else 'Disabled'
|
||||
|
||||
users_to_change = [user for user in tautulli_users if user['allow_guest'] != allow_guest]
|
||||
|
||||
if users_to_change:
|
||||
for user in users_to_change:
|
||||
# Redefine ALL params because of Tautulli edit_user API bug
|
||||
params = {
|
||||
'cmd': 'edit_user',
|
||||
'user_id': user['user_id'],
|
||||
'friendly_name': user['friendly_name'],
|
||||
'custom_thumb': user['custom_thumb'],
|
||||
'keep_history': user['keep_history'],
|
||||
'allow_guest': allow_guest
|
||||
}
|
||||
changed_user = session.get(formatted_url, params=params)
|
||||
print(f"{string_representation} guest access for {user['friendly_name']}")
|
||||
else:
|
||||
print(f'No users to {string_representation.lower()[:-1]}')
|
@ -2,13 +2,14 @@
|
||||
"""
|
||||
Description: Purge Tautulli users that no longer exist as a friend in Plex
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, plexapi
|
||||
Requires: requests, plexapi, python3.6+
|
||||
"""
|
||||
|
||||
import requests
|
||||
from requests import Session
|
||||
from json.decoder import JSONDecodeError
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
|
||||
TAUTULLI_BASE_URL = ''
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_API_KEY = ''
|
||||
|
||||
PLEX_USERNAME = ''
|
||||
@ -20,22 +21,27 @@ BACKUP_DB = True
|
||||
# Do not edit past this line #
|
||||
account = MyPlexAccount(PLEX_USERNAME, PLEX_PASSWORD)
|
||||
|
||||
payload = {'apikey': TAUTULLI_API_KEY, 'cmd': 'get_user_names'}
|
||||
tautulli_users = requests.get('http://{}/api/v2'
|
||||
.format(TAUTULLI_BASE_URL), params=payload).json()['response']['data']
|
||||
session = Session()
|
||||
session.params = {'apikey': TAUTULLI_API_KEY}
|
||||
formatted_url = f'{TAUTULLI_URL}/api/v2'
|
||||
|
||||
request = session.get(formatted_url, params={'cmd': 'get_user_names'})
|
||||
|
||||
tautulli_users = None
|
||||
try:
|
||||
tautulli_users = request.json()['response']['data']
|
||||
except JSONDecodeError:
|
||||
exit("Error talking to Tautulli API, please check your TAUTULLI_URL")
|
||||
|
||||
plex_friend_ids = [friend.id for friend in account.users()]
|
||||
tautulli_user_ids = [user['user_id'] for user in tautulli_users]
|
||||
|
||||
removed_user_ids = [user_id for user_id in tautulli_user_ids if user_id not in plex_friend_ids]
|
||||
removed_users = [user for user in tautulli_users if user['user_id'] not in plex_friend_ids]
|
||||
|
||||
if BACKUP_DB:
|
||||
payload['cmd'] = 'backup_db'
|
||||
backup = requests.get('http://{}/api/v2'.format(TAUTULLI_BASE_URL), params=payload)
|
||||
backup = session.get(formatted_url, params={'cmd': 'backup_db'})
|
||||
|
||||
if removed_user_ids:
|
||||
payload['cmd'] = 'delete_user'
|
||||
|
||||
for user_id in removed_user_ids:
|
||||
payload['user_id'] = user_id
|
||||
remove_user = requests.get('http://{}/api/v2'.format(TAUTULLI_BASE_URL), params=payload)
|
||||
if removed_users:
|
||||
for user in removed_users:
|
||||
removed_user = session.get(formatted_url, params={'cmd': 'delete_user', 'user_id': user['user_id']})
|
||||
print(f"Removed {user['friendly_name']} from Tautulli")
|
||||
else:
|
||||
print('No users to remove')
|
||||
|
Loading…
Reference in New Issue
Block a user