Merge pull request #130 from DirtyCajunRice/master

Lots of Updates
This commit is contained in:
blacktwin 2019-01-02 21:39:39 -05:00 committed by GitHub
commit 472e438967
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 70 deletions

View File

@ -1,13 +1,13 @@
""" """
Description: Use conditions to kill a stream Description: Use conditions to kill a stream
Author: Blacktwin, Arcanemagus, Samwiseg00, JonnyWong16 Author: Blacktwin, Arcanemagus, Samwiseg0, JonnyWong16, DirtyCajunRice
Adding the script to Tautulli: Adding the script to Tautulli:
Taultulli > Settings > Notification Agents > Add a new notification agent > Tautulli > Settings > Notification Agents > Add a new notification agent >
Script Script
Configuration: Configuration:
Taultulli > Settings > Notification Agents > New Script > Configuration: Tautulli > Settings > Notification Agents > New Script > Configuration:
Script Folder: /path/to/your/scripts Script Folder: /path/to/your/scripts
Script File: ./kill_stream.py (Should be selectable in a dropdown list) Script File: ./kill_stream.py (Should be selectable in a dropdown list)
@ -16,19 +16,19 @@ Taultulli > Settings > Notification Agents > New Script > Configuration:
Save Save
Triggers: Triggers:
Taultulli > Settings > Notification Agents > New Script > Triggers: Tautulli > Settings > Notification Agents > New Script > Triggers:
Check: Playback Start and/or Playback Pause Check: Playback Start and/or Playback Pause
Save Save
Conditions: Conditions:
Taultulli > Settings > Notification Agents > New Script > Conditions: Tautulli > Settings > Notification Agents > New Script > Conditions:
Set Conditions: [{condition} | {operator} | {value} ] Set Conditions: [{condition} | {operator} | {value} ]
Save Save
Script Arguments: Script Arguments:
Taultulli > Settings > Notification Agents > New Script > Script Arguments: Tautulli > 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}
@ -46,11 +46,13 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
import os import os
import sys import sys
import argparse
import json import json
import time import time
import argparse
from datetime import datetime from datetime import datetime
import requests from requests import Session
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
TAUTULLI_URL = '' TAUTULLI_URL = ''
@ -109,13 +111,15 @@ def debug_dump_vars():
.rjust(len(TAUTULLI_APIKEY), "x")) .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. """Get a list of all current streams.
Parameters Parameters
---------- ----------
user_id : int user_id : int
The ID of the user to grab sessions for. The ID of the user to grab sessions for.
tautulli : obj
Tautulli object.
Returns Returns
------- -------
objects objects
@ -131,17 +135,17 @@ def get_all_streams(user_id=None):
return streams 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""" """Decides which notifier type to use"""
if opts.notify and opts.richMessage: if all_opts.notify and all_opts.richMessage:
rich_notify(opts.notify, opts.richMessage, opts.richColor, kill_type, rich_notify(all_opts.notify, all_opts.richMessage, all_opts.richColor, kill_type,
opts.serverName, opts.plexUrl, opts.posterUrl, message, stream) all_opts.serverName, all_opts.plexUrl, all_opts.posterUrl, message, stream, tautulli)
elif opts.notify: elif all_opts.notify:
basic_notify(opts.notify, opts.sessionId, opts.username, message) 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, 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 """Decides which rich notifier type to use. Set default values for empty variables
Parameters 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. The ID of the user to grab sessions for.
rich_type : str rich_type : str
Contains 'discord' or 'slack'. Contains 'discord' or 'slack'.
color : Union[int, str]
Hex string or integer representation of color.
kill_type : str kill_type : str
The kill type used. The kill type used.
server_name : str 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. Message sent to the client.
stream : obj stream : obj
Stream object. Stream object.
tautulli : obj
Tautulli object.
""" """
notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream) notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream)
# Initialize Variables
title = ''
footer = ''
# Set a default server_name if none is provided # Set a default server_name if none is provided
if server_name is None: if server_name is None:
server_name = 'Plex Server' server_name = 'Plex Server'
# Set a defult color if none is provided # Set a default color if none is provided
if color is None: if color is None:
color = '#E5A00D' color = '#E5A00D'
# Set a defult plexUrl if none is provided # Set a default plexUrl if none is provided
if plex_url is None: if plex_url is None:
plex_url = 'https://app.plex.tv' 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: if poster_url is None:
poster_url = TAUTULLI_ICON 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) 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""" """Basic notifier"""
notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream) notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream)
@ -225,11 +235,11 @@ class Tautulli:
self.apikey = apikey self.apikey = apikey
self.debug = debug self.debug = debug
self.session = requests.Session() self.session = Session()
self.adapters = requests.adapters.HTTPAdapter(max_retries=3, self.adapters = HTTPAdapter(max_retries=3,
pool_connections=1, pool_connections=1,
pool_maxsize=1, pool_maxsize=1,
pool_block=True) pool_block=True)
self.session.mount('http://', self.adapters) self.session.mount('http://', self.adapters)
self.session.mount('https://', self.adapters) self.session.mount('https://', self.adapters)
@ -240,14 +250,14 @@ class Tautulli:
import urllib3 import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 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['cmd'] = cmd
payload['apikey'] = self.apikey payload['apikey'] = self.apikey
try: try:
response = self.session.request(method, self.url + '/api/v2', params=payload) response = self.session.request(method, self.url + '/api/v2', params=payload)
except: except RequestException as e:
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL?".format(cmd)) print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
if self.debug: if self.debug:
traceback.print_exc() traceback.print_exc()
return return
@ -303,11 +313,12 @@ class Tautulli:
class Stream: class Stream:
def __init__(self, session_id=None, user_id=None, username=None, tautulli=None, session=None): 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.session_id = session_id
self.user_id = user_id self.user_id = user_id
self.username = username self.username = username
self.session_exists = False self.session_exists = False
self.tautulli = tautulli self.tautulli = tautulli
if session is not None: if session is not None:
@ -579,38 +590,38 @@ if __name__ == "__main__":
sys.exit(1) sys.exit(1)
if opts.debug: if opts.debug:
# Import traceback to get more deatiled information # Import traceback to get more detailed information
import traceback import traceback
# Dump the ENVs passed from tatutulli # Dump the ENVs passed from tautulli
debug_dump_vars() debug_dump_vars()
# Create a Tautulli instance # 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 # 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 # Only pull all stream info if using richMessage
if opts.notify and opts.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: if opts.killMessage:
message = ' '.join(opts.killMessage) kill_message = ' '.join(opts.killMessage)
else: else:
message = 'The server owner has ended the stream.' kill_message = 'The server owner has ended the stream.'
if opts.jbop == 'stream': if opts.jbop == 'stream':
stream.terminate(message) tautulli_stream.terminate(kill_message)
notify(opts, message, 'Stream', stream) notify(opts, kill_message, 'Stream', tautulli_stream, tautulli_server)
elif opts.jbop == 'allStreams': elif opts.jbop == 'allStreams':
streams = get_all_streams(opts.userId) all_streams = get_all_streams(tautulli_server, opts.userId)
for stream in streams: for a_stream in all_streams:
tautulli.terminate_session(session_id=stream.session_id, message=message) tautulli_server.terminate_session(session_id=a_stream.session_id, message=kill_message)
notify(opts, message, 'All Streams', stream) notify(opts, kill_message, 'All Streams', a_stream, tautulli_server)
elif opts.jbop == 'paused': 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: if killed_stream:
notify(opts, message, 'Paused', stream) notify(opts, kill_message, 'Paused', tautulli_stream, tautulli_server)

View File

@ -6,7 +6,10 @@ Killing streams is a Plex Pass only feature. So these scripts will **only** work
### Kill transcodes ### Kill transcodes
Triggers: Playback Start Triggers:
* Playback Start
* Transcode Decision Change
Conditions: \[ `Transcode Decision` | `is` | `transcode` \] Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
Arguments: Arguments:
@ -59,6 +62,18 @@ Arguments:
--jbop stream --username {username} --sessionId {session_id} --killMessage 'You are only allowed 3 streams.' --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 ### IP Whitelist
Triggers: Playback Start Triggers: Playback Start
@ -81,7 +96,10 @@ Arguments:
### Kill transcode by library ### Kill transcode by library
Triggers: Playback Start Triggers:
* Playback Start
* Transcode Decision Change
Conditions: Conditions:
* \[ `Transcode Decision` | `is` | `transcode` \] * \[ `Transcode Decision` | `is` | `transcode` \]
* \[ `Library Name` | `is` | `4K Movies` \] * \[ `Library Name` | `is` | `4K Movies` \]
@ -93,7 +111,10 @@ Arguments:
### Kill transcode by original resolution ### Kill transcode by original resolution
Triggers: Playback Start Triggers:
* Playback Start
* Transcode Decision Change
Conditions: Conditions:
* \[ `Transcode Decision` | `is` | `transcode` \] * \[ `Transcode Decision` | `is` | `transcode` \]
* \[ `Video Resolution` | `is` | `1080 or 720`\] * \[ `Video Resolution` | `is` | `1080 or 720`\]
@ -105,7 +126,10 @@ Arguments:
### Kill transcode by bitrate ### Kill transcode by bitrate
Triggers: Playback Start Triggers:
* Playback Start
* Transcode Decision Change
Conditions: Conditions:
* \[ `Transcode Decision` | `is` | `transcode` \] * \[ `Transcode Decision` | `is` | `transcode` \]
* \[ `Bitrate` | `is greater than` | `4000` \] * \[ `Bitrate` | `is greater than` | `4000` \]
@ -137,7 +161,10 @@ Arguments:
### Kill transcodes and send a notification to agent 1 ### Kill transcodes and send a notification to agent 1
Triggers: Playback Start Triggers:
* Playback Start
* Transcode Decision Change
Conditions: \[ `Transcode Decision` | `is` | `transcode` \] Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
Arguments: Arguments:
@ -147,7 +174,10 @@ Arguments:
### Kill transcodes using the default message ### Kill transcodes using the default message
Triggers: Playback Start Triggers:
* Playback Start
* Transcode Decision Change
Conditions: \[ `Transcode Decision` | `is` | `transcode` \] Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
Arguments: Arguments:
@ -211,3 +241,11 @@ Tautulli > Script Agent > Script > Tautulli > Webhook Agent > Discord/Slack
### Debug ### Debug
Add `--debug` to enable debug logging. 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` \]

View File

@ -3,4 +3,5 @@
# pip install -r requirements.txt # pip install -r requirements.txt
#--------------------------------------------------------- #---------------------------------------------------------
requests requests
plexapi plexapi
urllib3

View 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]}')

View File

@ -2,13 +2,14 @@
""" """
Description: Purge Tautulli users that no longer exist as a friend in Plex Description: Purge Tautulli users that no longer exist as a friend in Plex
Author: DirtyCajunRice 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 from plexapi.myplex import MyPlexAccount
TAUTULLI_BASE_URL = '' TAUTULLI_URL = ''
TAUTULLI_API_KEY = '' TAUTULLI_API_KEY = ''
PLEX_USERNAME = '' PLEX_USERNAME = ''
@ -20,22 +21,27 @@ BACKUP_DB = True
# Do not edit past this line # # Do not edit past this line #
account = MyPlexAccount(PLEX_USERNAME, PLEX_PASSWORD) account = MyPlexAccount(PLEX_USERNAME, PLEX_PASSWORD)
payload = {'apikey': TAUTULLI_API_KEY, 'cmd': 'get_user_names'} session = Session()
tautulli_users = requests.get('http://{}/api/v2' session.params = {'apikey': TAUTULLI_API_KEY}
.format(TAUTULLI_BASE_URL), params=payload).json()['response']['data'] 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()] plex_friend_ids = [friend.id for friend in account.users()]
tautulli_user_ids = [user['user_id'] for user in tautulli_users] removed_users = [user for user in tautulli_users if user['user_id'] not in plex_friend_ids]
removed_user_ids = [user_id for user_id in tautulli_user_ids if user_id not in plex_friend_ids]
if BACKUP_DB: if BACKUP_DB:
payload['cmd'] = 'backup_db' backup = session.get(formatted_url, params={'cmd': 'backup_db'})
backup = requests.get('http://{}/api/v2'.format(TAUTULLI_BASE_URL), params=payload)
if removed_user_ids: if removed_users:
payload['cmd'] = 'delete_user' for user in removed_users:
removed_user = session.get(formatted_url, params={'cmd': 'delete_user', 'user_id': user['user_id']})
for user_id in removed_user_ids: print(f"Removed {user['friendly_name']} from Tautulli")
payload['user_id'] = user_id else:
remove_user = requests.get('http://{}/api/v2'.format(TAUTULLI_BASE_URL), params=payload) print('No users to remove')