Merge remote-tracking branch 'origin/master'

# Conflicts:
#	notify/notify_added_custom.py
#	notify/notify_on_added.py
#	utility/sync_watch_status.py
This commit is contained in:
Blacktwin 2019-06-25 14:40:47 -04:00
commit 165e851cbc
57 changed files with 1226 additions and 585 deletions

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Description: Create and share playlists based on Most Popular TV/Movies from Tautulli Description: Create and share playlists based on Most Popular TV/Movies from Tautulli
and Aired this day in history. and Aired this day in history.
@ -43,7 +46,7 @@ optional arguments:
Example: Example:
Use with cron or task to schedule runs Use with cron or task to schedule runs
Create Aired Today Playlist from Movies and TV Shows libraries for admin user Create Aired Today Playlist from Movies and TV Shows libraries for admin user
python playlist_manager.py --jbop historyToday --libraries Movies "TV Shows" --action add python playlist_manager.py --jbop historyToday --libraries Movies "TV Shows" --action add
@ -61,29 +64,29 @@ optional arguments:
Create 10 Most Popular Movies (60 days) Playlist and share to users bob and Black Twin Create 10 Most Popular Movies (60 days) Playlist and share to users bob and Black Twin
python playlist_manager.py --jbop popularMovies --action add --users bob "Black Twin" --days 60 --top 10 python playlist_manager.py --jbop popularMovies --action add --users bob "Black Twin" --days 60 --top 10
Show 5 Most Popular TV Shows (30 days) Playlist Show 5 Most Popular TV Shows (30 days) Playlist
python playlist_manager.py --jbop popularTv --action show python playlist_manager.py --jbop popularTv --action show
Show all users current playlists Show all users current playlists
python playlist_manager.py --action show --allUsers python playlist_manager.py --action show --allUsers
Share existing admin Playlists "My Custom Playlist" and "Another Playlist" with all users Share existing admin Playlists "My Custom Playlist" and "Another Playlist" with all users
python playlist_manager.py --action share --allUsers --playlists "My Custom Playlist" "Another Playlist" python playlist_manager.py --action share --allUsers --playlists "My Custom Playlist" "Another Playlist"
Search and Filter; Search and Filter;
metadata_field_name = title, summary, etc. metadata_field_name = title, summary, etc.
--search {metadata_field_name}=value --search {metadata_field_name}=value
search through metadata field for existence of value. search through metadata field for existence of value.
--search {metadata_field_name}=value1,value2,* --search {metadata_field_name}=value1,value2,*
search through metadata field for existence of values. search through metadata field for existence of values.
*comma separated for AND (value1 AND value2 AND *) *comma separated for AND (value1 AND value2 AND *)
Excluding; Excluding;
--user becomes excluded if --allUsers is set --user becomes excluded if --allUsers is set
@ -106,14 +109,14 @@ import unicodedata
from collections import Counter from collections import Counter
from plexapi.server import PlexServer, CONFIG from plexapi.server import PlexServer, CONFIG
### EDIT SETTINGS ### # ### EDIT SETTINGS ###
PLEX_URL = '' PLEX_URL = ''
PLEX_TOKEN = '' PLEX_TOKEN = ''
TAUTULLI_URL = '' TAUTULLI_URL = ''
TAUTULLI_APIKEY = '' TAUTULLI_APIKEY = ''
## CODE BELOW ## # ## CODE BELOW ##
if not PLEX_URL: if not PLEX_URL:
PLEX_URL = CONFIG.data['auth'].get('server_baseurl') PLEX_URL = CONFIG.data['auth'].get('server_baseurl')
@ -150,6 +153,7 @@ playlist_lst = [x.title for x in plex.playlists()]
today = datetime.datetime.now().date() today = datetime.datetime.now().date()
weeknum = datetime.date(today.year, today.month, today.day).isocalendar()[1] weeknum = datetime.date(today.year, today.month, today.day).isocalendar()[1]
def actions(): def actions():
""" """
add - create new playlist for admin or users add - create new playlist for admin or users
@ -172,7 +176,7 @@ def selectors():
'custom': '{custom} Playlist', 'custom': '{custom} Playlist',
'random': '{count} Random {libraries} Playlist' 'random': '{count} Random {libraries} Playlist'
} }
return selections return selections
@ -203,7 +207,7 @@ def exclusions(all_true, select, all_items):
for x in select: for x in select:
all_items.remove(x) all_items.remove(x)
output = all_items output = all_items
elif isinstance(all_items, dict): elif isinstance(all_items, dict):
output = {} output = {}
if all_true and not select: if all_true and not select:
@ -216,7 +220,7 @@ def exclusions(all_true, select, all_items):
for key, value in all_items.items(): for key, value in all_items.items():
if value not in select: if value not in select:
output[key] = value output[key] = value
return output return output
@ -226,7 +230,7 @@ def get_home_stats(time_range, stats_count):
'cmd': 'get_home_stats', 'cmd': 'get_home_stats',
'time_range': time_range, 'time_range': time_range,
'stats_count': stats_count, 'stats_count': stats_count,
'stats_type': 0} # stats_type = plays 'stats_type': 0} # stats_type = plays
try: try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
@ -269,14 +273,14 @@ def sort_by_dates(video, date_type):
return [[video.ratingKey] + [str(video.originallyAvailableAt)]] return [[video.ratingKey] + [str(video.originallyAvailableAt)]]
# todo-me return object # todo-me return object
except Exception as e: except Exception:
# print(e) # print(e)
return return
def multi_filter_search(keyword_dict, library, search_eps=None): def multi_filter_search(keyword_dict, library, search_eps=None):
"""Allowing for multiple filter or search values """Allowing for multiple filter or search values
Parameters Parameters
---------- ----------
keyword_dict: dict keyword_dict: dict
@ -318,17 +322,18 @@ def multi_filter_search(keyword_dict, library, search_eps=None):
for episode in show.episodes(**{key: values}): for episode in show.episodes(**{key: values}):
ep_lst += [episode.ratingKey] ep_lst += [episode.ratingKey]
multi_lst += ep_lst multi_lst += ep_lst
else: else:
multi_lst += [item.ratingKey for item in library.all(**{key: values})] multi_lst += [item.ratingKey for item in library.all(**{key: values})]
counts = Counter(multi_lst) counts = Counter(multi_lst)
# Use amount of keywords to check that all keywords were found in results # Use amount of keywords to check that all keywords were found in results
search_lst = [id for id in multi_lst if counts[id] >= keyword_count] search_lst = [id for id in multi_lst if counts[id] >= keyword_count]
return list(set(search_lst)) return list(set(search_lst))
def get_content(libraries, jbop, filters=None, search=None, limit=None): def get_content(libraries, jbop, filters=None, search=None, limit=None):
"""Get all movies or episodes from LIBRARY_NAME """Get all movies or episodes from LIBRARY_NAME.
Parameters Parameters
---------- ----------
@ -342,6 +347,7 @@ def get_content(libraries, jbop, filters=None, search=None, limit=None):
list list
Sorted list of Movie and episodes that Sorted list of Movie and episodes that
aired on today's date. aired on today's date.
""" """
child_lst = [] child_lst = []
filter_lst = [] filter_lst = []
@ -378,7 +384,7 @@ def get_content(libraries, jbop, filters=None, search=None, limit=None):
child_lst += filter_lst child_lst += filter_lst
if keywords and filters: if keywords and filters:
child_lst += list(set(filter_lst) & set(search_lst)) child_lst += list(set(filter_lst) & set(search_lst))
elif library_type == 'show': elif library_type == 'show':
# Decisions to stack filter and search # Decisions to stack filter and search
if keywords: if keywords:
@ -402,7 +408,7 @@ def get_content(libraries, jbop, filters=None, search=None, limit=None):
for episode in show.episodes(): for episode in show.episodes():
filter_lst += [episode.ratingKey] filter_lst += [episode.ratingKey]
child_lst += filter_lst child_lst += filter_lst
if keywords and filters: if keywords and filters:
child_lst += list(set(filter_lst) & set(search_lst)) child_lst += list(set(filter_lst) & set(search_lst))
else: else:
@ -410,9 +416,9 @@ def get_content(libraries, jbop, filters=None, search=None, limit=None):
# Keep only results found from both search and filters # Keep only results found from both search and filters
if keywords and filters: if keywords and filters:
child_lst = list(set(i for i in child_lst if child_lst.count(i) > 1)) child_lst = list(set(i for i in child_lst if child_lst.count(i) > 1))
play_lst = child_lst play_lst = child_lst
else: else:
for library in libraries.keys(): for library in libraries.keys():
plex_library = plex.library.sectionByID(library) plex_library = plex.library.sectionByID(library)
@ -440,7 +446,7 @@ def get_content(libraries, jbop, filters=None, search=None, limit=None):
# Sort by original air date, oldest first # Sort by original air date, oldest first
# todo-me move sorting and add more sorting options # todo-me move sorting and add more sorting options
aired_lst = sorted(child_lst, key=operator.itemgetter(1)) aired_lst = sorted(child_lst, key=operator.itemgetter(1))
# Remove date used for sorting # Remove date used for sorting
play_lst = [x[0] for x in aired_lst] play_lst = [x[0] for x in aired_lst]
else: else:
@ -474,7 +480,7 @@ def build_playlist(jbop, libraries=None, days=None, top=None, filters=None, sear
for stat in home_stats: for stat in home_stats:
if stat['stat_id'] in ['popular_tv', 'popular_movies']: if stat['stat_id'] in ['popular_tv', 'popular_movies']:
keys_list += [x['rating_key'] for x in stat['rows'] if keys_list += [x['rating_key'] for x in stat['rows'] if
str(x['section_id']) in libraries.keys()] str(x['section_id']) in libraries.keys()]
else: else:
try: try:
keys_list = get_content(libraries, jbop, filters, search, limit) keys_list = get_content(libraries, jbop, filters, search, limit)
@ -525,11 +531,12 @@ def show_playlist(playlist_title, playlist_keys):
title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').translate(None, "'") title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').translate(None, "'")
playlist_list.append(title) playlist_list.append(title)
print(u"Contents of Playlist {title}:\n{playlist}".format(title=playlist_title, print(u"Contents of Playlist {title}:\n{playlist}".format(
playlist=', '.join(playlist_list))) title=playlist_title,
playlist=', '.join(playlist_list)))
exit() exit()
def create_playlist(playlist_title, playlist_keys, server, user): def create_playlist(playlist_title, playlist_keys, server, user):
""" """
Parameters Parameters
@ -552,13 +559,13 @@ def create_playlist(playlist_title, playlist_keys, server, user):
playlist_list.append(episode) playlist_list.append(episode)
else: else:
playlist_list.append(plex_obj) playlist_list.append(plex_obj)
except Exception as e: except Exception:
try: try:
obj = plex.fetchItem(key) obj = plex.fetchItem(key)
print("{} may not have permission to this title: {}".format(user, obj.title)) print("{} may not have permission to this title: {}".format(user, obj.title))
# print("Error: {}".format(e)) # print("Error: {}".format(e))
pass pass
except Exception as e: except Exception:
print('Rating Key: {}, may have been deleted or moved.'.format(key)) print('Rating Key: {}, may have been deleted or moved.'.format(key))
# print("Error: {}".format(e)) # print("Error: {}".format(e))
@ -578,7 +585,7 @@ def delete_playlist(playlist_dict, title):
""" """
server = playlist_dict['server'] server = playlist_dict['server']
user = playlist_dict['user'] user = playlist_dict['user']
try: try:
# todo-me this needs improvement # todo-me this needs improvement
for playlist in server.playlists(): for playlist in server.playlists():
@ -595,14 +602,14 @@ def delete_playlist(playlist_dict, title):
print("...Deleted Playlist: {playlist.title} for '{user}'." print("...Deleted Playlist: {playlist.title} for '{user}'."
.format(playlist=playlist, user=user)) .format(playlist=playlist, user=user))
except: except Exception:
# print("Playlist not found on '{user}' account".format(user=user)) # print("Playlist not found on '{user}' account".format(user=user))
pass pass
def create_title(jbop, libraries, days, filters, search, limit): def create_title(jbop, libraries, days, filters, search, limit):
""" """
Parameters Parameters
---------- ----------
jbop: str jbop: str
@ -672,9 +679,9 @@ def create_title(jbop, libraries, days, filters, search, limit):
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(
parser = argparse.ArgumentParser(description="Create, share, and clean Playlists for users.", description="Create, share, and clean Playlists for users.",
formatter_class = argparse.RawTextHelpFormatter) formatter_class=argparse.RawTextHelpFormatter)
# todo-me use parser grouping instead of choices for action and jbop? # todo-me use parser grouping instead of choices for action and jbop?
parser.add_argument('--jbop', choices=selectors().keys(), metavar='', parser.add_argument('--jbop', choices=selectors().keys(), metavar='',
help='Playlist selector.\n' help='Playlist selector.\n'
@ -715,7 +722,7 @@ if __name__ == "__main__":
parser.add_argument('--search', action='append', type=lambda kv: kv.split("="), parser.add_argument('--search', action='append', type=lambda kv: kv.split("="),
help='Search non-filtered metadata fields for keywords ' help='Search non-filtered metadata fields for keywords '
'in title, summary, etc.') 'in title, summary, etc.')
opts = parser.parse_args() opts = parser.parse_args()
title = '' title = ''
@ -739,7 +746,7 @@ if __name__ == "__main__":
# If filter key used more than once than consider filtering values with OR statement # If filter key used more than once than consider filtering values with OR statement
if filter_count > 1: if filter_count > 1:
filters_lst = [] filters_lst = []
filters = dict(opts.filter) filters = dict(opts.filter)
for k, v in filters.items(): for k, v in filters.items():
# If comma separated filter then consider filtering values with AND statement # If comma separated filter then consider filtering values with AND statement
@ -756,10 +763,10 @@ if __name__ == "__main__":
# Defining libraries # Defining libraries
libraries = exclusions(opts.allLibraries, opts.libraries, sections_dict) libraries = exclusions(opts.allLibraries, opts.libraries, sections_dict)
# Defining selected playlists # Defining selected playlists
selected_playlists = exclusions(opts.allPlaylists, opts.playlists, playlist_lst) selected_playlists = exclusions(opts.allPlaylists, opts.playlists, playlist_lst)
# Create user server objects # Create user server objects
if users: if users:
for user in users: for user in users:
@ -776,12 +783,13 @@ if __name__ == "__main__":
'user': user, 'user': user,
'user_selected': user_selected, 'user_selected': user_selected,
'all_playlists': all_playlists}) 'all_playlists': all_playlists})
if opts.self or not users: if opts.self or not users:
playlist_dict['data'].append({'server': plex, playlist_dict['data'].append({
'user': 'admin', 'server': plex,
'user_selected': selected_playlists, 'user': 'admin',
'all_playlists': playlist_lst}) 'user_selected': selected_playlists,
'all_playlists': playlist_lst})
if not opts.jbop and opts.action == 'show': if not opts.jbop and opts.action == 'show':
print("Displaying the user's playlist(s)...") print("Displaying the user's playlist(s)...")
@ -790,7 +798,7 @@ if __name__ == "__main__":
playlists = data['all_playlists'] playlists = data['all_playlists']
print("{}'s current playlist(s): {}".format(user, ', '.join(playlists))) print("{}'s current playlist(s): {}".format(user, ', '.join(playlists)))
exit() exit()
if libraries: if libraries:
title = create_title(opts.jbop, libraries, opts.days, filters, search, opts.limit) title = create_title(opts.jbop, libraries, opts.days, filters, search, opts.limit)
keys_list = build_playlist(opts.jbop, libraries, opts.days, opts.top, filters, search, opts.limit) keys_list = build_playlist(opts.jbop, libraries, opts.days, opts.top, filters, search, opts.limit)
@ -801,18 +809,18 @@ if __name__ == "__main__":
for data in playlist_dict['data']: for data in playlist_dict['data']:
titles = data['user_selected'] titles = data['user_selected']
delete_playlist(data, titles) delete_playlist(data, titles)
# Check if limit exist and if it's greater than the pulled list of rating keys # Check if limit exist and if it's greater than the pulled list of rating keys
if opts.limit and len(keys_list) > int(opts.limit): if opts.limit and len(keys_list) > int(opts.limit):
if opts.jbop == 'random': if opts.jbop == 'random':
keys_list = random.sample((keys_list), opts.limit) keys_list = random.sample((keys_list), opts.limit)
else: else:
keys_list = keys_list[:opts.limit] keys_list = keys_list[:opts.limit]
# Setting custom name if provided # Setting custom name if provided
if opts.name: if opts.name:
title = opts.name title = opts.name
if opts.jbop and opts.action == 'show': if opts.jbop and opts.action == 'show':
show_playlist(title, keys_list) show_playlist(title, keys_list)
@ -823,7 +831,7 @@ if __name__ == "__main__":
print('Creating playlist(s)...') print('Creating playlist(s)...')
for data in playlist_dict['data']: for data in playlist_dict['data']:
create_playlist(title, keys_list, data['server'], data['user']) create_playlist(title, keys_list, data['server'], data['user'])
if opts.action == 'add': if opts.action == 'add':
print('Creating playlist(s)...') print('Creating playlist(s)...')
for data in playlist_dict['data']: for data in playlist_dict['data']:

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# #
# Author: Bailey Belvis (https://github.com/philosowaffle) # Author: Bailey Belvis (https://github.com/philosowaffle)
# #
@ -6,18 +9,16 @@
# #
# - Enable `Upload Posters to Imgur for Notifications` - required for lights to match the posters color scheme # - Enable `Upload Posters to Imgur for Notifications` - required for lights to match the posters color scheme
# - Triggers - PlexLifx supports the following triggers, enable the ones you are interested in. # - Triggers - PlexLifx supports the following triggers, enable the ones you are interested in.
# - Notify on Playback Start # - Notify on Playback Start
# - Notify on Playback Stop # - Notify on Playback Stop
# - Notify on Playback Resume # - Notify on Playback Resume
# - Notify on Playback Pause # - Notify on Playback Pause
# #
# - Copy paste the following line to each of the Triggers you enabled (found on the Arguments tab): # - Copy paste the following line to each of the Triggers you enabled (found on the Arguments tab):
# -a {action} -mt {media_type} -mi {machine_id} -rk {rating_key} -pu {poster_url} # -a {action} -mt {media_type} -mi {machine_id} -rk {rating_key} -pu {poster_url}
#
import os import os
import sys
import logging import logging
import hashlib
import shutil import shutil
import numpy import numpy
import argparse import argparse
@ -99,10 +100,10 @@ filtered_players = [] if PlayerUUIDs == "none" else PlayerUUIDs.split(',')
logger.debug("Filtered Players: " + filtered_players.__str__()) logger.debug("Filtered Players: " + filtered_players.__str__())
events = [ events = [
'play', 'play',
'pause', 'pause',
'resume', 'resume',
'stop' 'stop'
] ]
############################## ##############################
@ -115,33 +116,33 @@ num_colors = NumColors if NumColors else 4
color_quality = ColorQuality if ColorQuality else 1 color_quality = ColorQuality if ColorQuality else 1
if not APIKey: if not APIKey:
logger.error("Missing LIFX API Key") logger.error("Missing LIFX API Key")
exit(1) exit(1)
else: else:
lifx_api_key = APIKey lifx_api_key = APIKey
logger.debug("LIFX API Key: " + lifx_api_key) logger.debug("LIFX API Key: " + lifx_api_key)
pifx = PIFX(lifx_api_key) pifx = PIFX(lifx_api_key)
lights = [] lights = []
if Lights: if Lights:
lights_use_name = True lights_use_name = True
lights = Lights.split(',') lights = Lights.split(',')
tmp = [] tmp = []
for light in lights: for light in lights:
tmp.append(light.strip()) tmp.append(light.strip())
lights = tmp lights = tmp
else: else:
lights_detail = pifx.list_lights() lights_detail = pifx.list_lights()
for light in lights_detail: for light in lights_detail:
lights.append(light['id']) lights.append(light['id'])
shuffle(lights) shuffle(lights)
scenes_details = pifx.list_scenes() scenes_details = pifx.list_scenes()
scenes = dict() scenes = dict()
for scene in scenes_details: for scene in scenes_details:
scenes[scene['name']] = scene['uuid'] scenes[scene['name']] = scene['uuid']
logger.debug(scenes) logger.debug(scenes)
logger.debug(lights) logger.debug(lights)
@ -154,7 +155,7 @@ default_play_uuid = scenes[default_play_theme]
number_of_lights = len(lights) number_of_lights = len(lights)
if number_of_lights < num_colors: if number_of_lights < num_colors:
num_colors = number_of_lights num_colors = number_of_lights
light_groups = numpy.array_split(numpy.array(lights), num_colors) light_groups = numpy.array_split(numpy.array(lights), num_colors)
@ -167,15 +168,15 @@ logger.debug("Color Quality: " + color_quality.__str__())
############################## ##############################
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
p.add_argument('-a', '--action', action='store', default='', p.add_argument('-a', '--action', action='store', default='',
help='The action that triggered the script.') help='The action that triggered the script.')
p.add_argument('-mt', '--media_type', action='store', default='', p.add_argument('-mt', '--media_type', action='store', default='',
help='The media type of the media being played.') help='The media type of the media being played.')
p.add_argument('-mi', '--machine_id', action='store', default='', p.add_argument('-mi', '--machine_id', action='store', default='',
help='The machine id of where the media is playing.') help='The machine id of where the media is playing.')
p.add_argument('-rk', '--rating_key', action='store', default='', p.add_argument('-rk', '--rating_key', action='store', default='',
help='The unique identifier for the media.') help='The unique identifier for the media.')
p.add_argument('-pu', '--poster_url', action='store', default='', p.add_argument('-pu', '--poster_url', action='store', default='',
help='The poster url for the media playing.') help='The poster url for the media playing.')
parser = p.parse_args() parser = p.parse_args()
@ -196,22 +197,22 @@ logger.debug("Media Guid: " + media_guid)
logger.debug("Poster Url: " + poster_url) logger.debug("Poster Url: " + poster_url)
# Only perform action for event play/pause/resume/stop for TV and Movies # Only perform action for event play/pause/resume/stop for TV and Movies
if not event in events: if event not in events:
logger.debug("Invalid action: " + event) logger.debug("Invalid action: " + event)
exit() exit()
if (media_type != "movie") and (media_type != "episode"): if (media_type != "movie") and (media_type != "episode"):
logger.debug("Media type was not movie or episode, ignoring.") logger.debug("Media type was not movie or episode, ignoring.")
exit() exit()
# If we configured only specific players to be able to play with the lights # If we configured only specific players to be able to play with the lights
if filtered_players: if filtered_players:
try: try:
if player_uuid not in filtered_players: if player_uuid not in filtered_players:
logger.info(player_uuid + " player is not able to play with the lights") logger.info(player_uuid + " player is not able to play with the lights")
exit() exit()
except Exception as e: except Exception as e:
logger.error("Failed to check uuid - " + e.__str__()) logger.error("Failed to check uuid - " + e.__str__())
# Setup Thumbnail directory paths # Setup Thumbnail directory paths
upload_folder = os.getcwd() + '\\tmp' upload_folder = os.getcwd() + '\\tmp'
@ -219,63 +220,63 @@ thumb_folder = os.path.join(upload_folder, media_guid)
thumb_path = os.path.join(thumb_folder, "thumb.jpg") thumb_path = os.path.join(thumb_folder, "thumb.jpg")
if event == 'stop': if event == 'stop':
if os.path.exists(thumb_folder): if os.path.exists(thumb_folder):
logger.debug("Removing Directory: " + thumb_folder) logger.debug("Removing Directory: " + thumb_folder)
shutil.rmtree(thumb_folder) shutil.rmtree(thumb_folder)
pifx.activate_scene(default_pause_uuid) pifx.activate_scene(default_pause_uuid)
exit() exit()
if event == 'pause': if event == 'pause':
pifx.activate_scene(default_pause_uuid) pifx.activate_scene(default_pause_uuid)
exit() exit()
if event == 'play' or event == "resume": if event == 'play' or event == "resume":
# If the file already exists then we don't need to re-upload the image # If the file already exists then we don't need to re-upload the image
if not os.path.exists(thumb_folder): if not os.path.exists(thumb_folder):
try: try:
logger.debug("Making Directory: " + thumb_folder) logger.debug("Making Directory: " + thumb_folder)
os.makedirs(thumb_folder) os.makedirs(thumb_folder)
urllib.urlretrieve(poster_url, thumb_path) urllib.urlretrieve(poster_url, thumb_path)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
logger.info("No file found in request") logger.info("No file found in request")
pifx.activate_scene(default_play_uuid) pifx.activate_scene(default_play_uuid)
exit() exit()
# Determine Color Palette for Lights # Determine Color Palette for Lights
color_thief = ColorThief(thumb_path) color_thief = ColorThief(thumb_path)
if num_colors >= 2: if num_colors >= 2:
palette = color_thief.get_palette(color_count=num_colors, quality=color_quality) palette = color_thief.get_palette(color_count=num_colors, quality=color_quality)
else: else:
palette = [color_thief.get_color(quality=color_quality)] palette = [color_thief.get_color(quality=color_quality)]
logger.debug("Color Palette: " + palette.__str__()) logger.debug("Color Palette: " + palette.__str__())
# Set Color Palette # Set Color Palette
pifx.set_state(selector='all', power="off") pifx.set_state(selector='all', power="off")
for index in range(len(light_groups)): for index in range(len(light_groups)):
try: try:
color = palette[index] color = palette[index]
light_group = light_groups[index] light_group = light_groups[index]
logger.debug(light_group) logger.debug(light_group)
logger.debug(color) logger.debug(color)
color_rgb = ', '.join(str(c) for c in color) color_rgb = ', '.join(str(c) for c in color)
color_rgb = "rgb:" + color_rgb color_rgb = "rgb:" + color_rgb
color_rgb = color_rgb.replace(" ", "") color_rgb = color_rgb.replace(" ", "")
for light_id in light_group: for light_id in light_group:
if lights_use_name: if lights_use_name:
selector = "label:" + light_id selector = "label:" + light_id
else: else:
selector = light_id selector = light_id
logger.debug("Setting light: " + selector + " to color: " + color_rgb) logger.debug("Setting light: " + selector + " to color: " + color_rgb)
pifx.set_state(selector=selector, power="on", color=color_rgb, brightness=brightness, duration=duration) pifx.set_state(selector=selector, power="on", color=color_rgb, brightness=brightness, duration=duration)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
exit() exit()

View File

@ -1,11 +1,14 @@
''' #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
https://gist.github.com/blacktwin/4ccb79c7d01a95176b8e88bf4890cd2b https://gist.github.com/blacktwin/4ccb79c7d01a95176b8e88bf4890cd2b
''' """
from plexapi.server import PlexServer from plexapi.server import PlexServer
import random import random
import re import re
baseurl = 'http://localhost:32400' baseurl = 'http://localhost:32400'
token = 'xxxxx' token = 'xxxxx'
plex = PlexServer(baseurl, token) plex = PlexServer(baseurl, token)
@ -44,8 +47,11 @@ def sylco(word):
if word[-2:] == "es" or word[-2:] == "ed": if word[-2:] == "es" or word[-2:] == "ed":
doubleAndtripple_1 = len(re.findall(r'[eaoui][eaoui]', word)) doubleAndtripple_1 = len(re.findall(r'[eaoui][eaoui]', word))
if doubleAndtripple_1 > 1 or len(re.findall(r'[eaoui][^eaoui]', word)) > 1: if doubleAndtripple_1 > 1 or len(re.findall(r'[eaoui][^eaoui]', word)) > 1:
if word[-3:] == "ted" or word[-3:] == "tes" or word[-3:] == "ses" or word[-3:] == "ied" or word[ if word[-3:] == "ted" or \
-3:] == "ies": word[-3:] == "tes" or \
word[-3:] == "ses" or \
word[-3:] == "ied" or \
word[-3:] == "ies":
pass pass
else: else:
disc += 1 disc += 1
@ -140,8 +146,7 @@ def sylco(word):
if word in exception_add: if word in exception_add:
syls += 1 syls += 1
# calculate the output
# calculate the output
return numVowels - disc + syls return numVowels - disc + syls
@ -190,13 +195,13 @@ for x in LIBRARIES_LST:
m_lst = hi_build(sections_lst, 5) + hi_build(sections_lst, 7) + hi_build(sections_lst, 5) m_lst = hi_build(sections_lst, 5) + hi_build(sections_lst, 7) + hi_build(sections_lst, 5)
# to see word and syllable count uncomment below print. # to see word and syllable count uncomment below print.
#print(m_lst) # print(m_lst)
stanz1 = ' '.join(m_lst[0].keys()) stanz1 = ' '.join(m_lst[0].keys())
stanz2 = ' '.join(m_lst[1].keys()) stanz2 = ' '.join(m_lst[1].keys())
stanz3 = ' '.join(m_lst[2].keys()) stanz3 = ' '.join(m_lst[2].keys())
lines = stanz1,stanz2,stanz3 lines = stanz1, stanz2, stanz3
lines = '\n'.join(lines) lines = '\n'.join(lines)
print('') print('')
print(lines.lower()) print(lines.lower())

View File

@ -1,4 +1,7 @@
''' #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
If server admin stream is experiencing buffering and there are concurrent transcode streams from If server admin stream is experiencing buffering and there are concurrent transcode streams from
another user, kill concurrent transcode stream that has the lowest percent complete. Message in another user, kill concurrent transcode stream that has the lowest percent complete. Message in
kill stream will list why it was killed ('Server Admin's stream take priority and this user has X kill stream will list why it was killed ('Server Admin's stream take priority and this user has X
@ -11,7 +14,7 @@ Tautulli > Settings > Notification Agents > Scripts > Bell icon:
Tautulli > Settings > Notification Agents > Scripts > Gear icon: Tautulli > Settings > Notification Agents > Scripts > Gear icon:
Buffer Warnings: kill_else_if_buffering.py Buffer Warnings: kill_else_if_buffering.py
''' """
import requests import requests
from operator import itemgetter from operator import itemgetter
@ -19,7 +22,7 @@ import unicodedata
from plexapi.server import PlexServer from plexapi.server import PlexServer
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
PLEX_TOKEN = 'xxxx' PLEX_TOKEN = 'xxxx'
PLEX_URL = 'http://localhost:32400' PLEX_URL = 'http://localhost:32400'
@ -27,8 +30,8 @@ DEFAULT_REASON = 'Server Admin\'s stream takes priority and {user}(you) has {x}
' {user}\'s stream of {video} is {time}% complete. Should be finished in {comp} minutes. ' \ ' {user}\'s stream of {video} is {time}% complete. Should be finished in {comp} minutes. ' \
'Try again then.' 'Try again then.'
ADMIN_USER = ('Admin') # additional usernames can be added ('Admin', 'user2') ADMIN_USER = ('Admin') # Additional usernames can be added ('Admin', 'user2')
## # ##
sess = requests.Session() sess = requests.Session()
sess.verify = False sess.verify = False
@ -71,17 +74,17 @@ def main():
# Remove users with only 1 stream. Targeting users with multiple concurrent streams # Remove users with only 1 stream. Targeting users with multiple concurrent streams
filtered_dict = {key: value for key, value in user_dict.items() filtered_dict = {key: value for key, value in user_dict.items()
if len(value) is not 1} if len(value) != 1}
# Find who to kill and who will be finishing first. # Find who to kill and who will be finishing first.
if filtered_dict: if filtered_dict:
for users in filtered_dict.values(): for users in filtered_dict.values():
to_kill = min(users, key=itemgetter(1)) to_kill = min(users, key=itemgetter(1))
to_finish = max(users, key=itemgetter(1)) to_finish = max(users, key=itemgetter(1))
MESSAGE = DEFAULT_REASON.format(user=to_finish[3], x=len(filtered_dict.values()[0]), MESSAGE = DEFAULT_REASON.format(user=to_finish[3], x=len(filtered_dict.values()[0]),
video=to_finish[2], time=to_finish[1], comp=to_finish[4]) video=to_finish[2], time=to_finish[1], comp=to_finish[4])
print(MESSAGE) print(MESSAGE)
kill_session(to_kill[0], MESSAGE) kill_session(to_kill[0], MESSAGE)

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Description: Use conditions to kill a stream Description: Use conditions to kill a stream
Author: Blacktwin, Arcanemagus, Samwiseg0, JonnyWong16, DirtyCajunRice Author: Blacktwin, Arcanemagus, Samwiseg0, JonnyWong16, DirtyCajunRice
@ -372,8 +375,8 @@ class Stream:
if self.session_exists is False: if self.session_exists is False:
sys.stdout.write( sys.stdout.write(
"Session '{}' from user '{}' is no longer active " "Session '{}' from user '{}' is no longer active "
.format(self.session_id, self.username) .format(self.session_id, self.username) +
+ "on the server, stopping monitoring.\n") "on the server, stopping monitoring.\n")
return False return False
now = datetime.now() now = datetime.now()
@ -392,8 +395,8 @@ class Stream:
elif self.state == 'playing' or self.state == 'buffering': elif self.state == 'playing' or self.state == 'buffering':
sys.stdout.write( sys.stdout.write(
"Session '{}' from user '{}' has been resumed, " "Session '{}' from user '{}' has been resumed, "
.format(self.session_id, self.username) .format(self.session_id, self.username) +
+ "stopping monitoring.\n") "stopping monitoring.\n")
return False return False
@ -536,7 +539,7 @@ class Notification:
"value": message, "value": message,
"short": False "short": False
} }
], ],
"thumb_url": poster_url, "thumb_url": poster_url,
"footer": footer, "footer": footer,
"ts": time.time() "ts": time.time()
@ -562,8 +565,8 @@ if __name__ == "__main__":
parser.add_argument('--sessionId', parser.add_argument('--sessionId',
help='The unique identifier for the stream.') help='The unique identifier for the stream.')
parser.add_argument('--notify', type=int, parser.add_argument('--notify', type=int,
help='Notification Agent ID number to Agent to send ' help='Notification Agent ID number to Agent to ' +
+ 'notification.') 'send notification.')
parser.add_argument('--limit', type=int, default=(20 * 60), # 20 minutes parser.add_argument('--limit', type=int, default=(20 * 60), # 20 minutes
help='The time session is allowed to remain paused.') help='The time session is allowed to remain paused.')
parser.add_argument('--interval', type=int, default=30, parser.add_argument('--interval', type=int, default=30,

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Description: Limiting Plex users by plays, watches, or total time from Tautulli. Description: Limiting Plex users by plays, watches, or total time from Tautulli.
Author: Blacktwin, Arcanemagus Author: Blacktwin, Arcanemagus
@ -48,7 +51,7 @@ import argparse
from datetime import datetime from datetime import datetime
import sys import sys
import os import os
from plexapi.server import PlexServer, CONFIG from plexapi.server import PlexServer
from time import time as ttime from time import time as ttime
from time import sleep from time import sleep
@ -65,6 +68,7 @@ TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
TAUTULLI_ENCODING = os.getenv('TAUTULLI_ENCODING', 'UTF-8') TAUTULLI_ENCODING = os.getenv('TAUTULLI_ENCODING', 'UTF-8')
# Using CONFIG file # Using CONFIG file
# from plexapi.server import CONFIG
# PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL) # PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
# PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN) # PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
# TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl', TAUTULLI_URL) # TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl', TAUTULLI_URL)
@ -88,7 +92,7 @@ if sess.verify is False:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess) plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
lib_dict = {x.title : x.key for x in plex.library.sections()} lib_dict = {x.title: x.key for x in plex.library.sections()}
SELECTOR = ['watch', 'plays', 'time', 'limit'] SELECTOR = ['watch', 'plays', 'time', 'limit']
@ -144,7 +148,7 @@ def get_activity(session_id=None):
try: try:
req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = req.json() response = req.json()
if session_id: if session_id:
res_data = response['response']['data'] res_data = response['response']['data']
else: else:
@ -271,7 +275,7 @@ if __name__ == "__main__":
'Default: %(default)s') 'Default: %(default)s')
parser.add_argument('--duration', type=int, default=0, parser.add_argument('--duration', type=int, default=0,
help='Duration of item that triggered script agent.') help='Duration of item that triggered script agent.')
opts = parser.parse_args() opts = parser.parse_args()
total_limit = 0 total_limit = 0
@ -327,7 +331,7 @@ if __name__ == "__main__":
else: else:
print('Session; {} has been dropped. Stopping monitoring of stream.'.format(opts.sessionId)) print('Session; {} has been dropped. Stopping monitoring of stream.'.format(opts.sessionId))
exit() exit()
print('Total {} ({} + current item duration {}) is greater than limit ({}).' print('Total {} ({} + current item duration {}) is greater than limit ({}).'
.format(opts.jbop, total_jbop, opts.duration, total_limit)) .format(opts.jbop, total_jbop, opts.duration, total_limit))
terminate_session(opts.sessionId, message, opts.notify, opts.username) terminate_session(opts.sessionId, message, opts.notify, opts.username)
@ -346,16 +350,16 @@ if __name__ == "__main__":
if not message: if not message:
message = LIMIT_MESSAGE.format(delay=opts.delay) message = LIMIT_MESSAGE.format(delay=opts.delay)
ep_watched = [data['watched_status'] for data in history['data'] ep_watched = [data['watched_status'] for data in history['data']
if data['grandparent_rating_key'] == opts.grandparent_rating_key if data['grandparent_rating_key'] == opts.grandparent_rating_key and
and data['watched_status'] == 1] data['watched_status'] == 1]
if not ep_watched: if not ep_watched:
ep_watched = 0 ep_watched = 0
else: else:
ep_watched = sum(ep_watched) ep_watched = sum(ep_watched)
stopped_time = [data['stopped'] for data in history['data'] stopped_time = [data['stopped'] for data in history['data']
if data['grandparent_rating_key'] == opts.grandparent_rating_key if data['grandparent_rating_key'] == opts.grandparent_rating_key and
and data['watched_status'] == 1] data['watched_status'] == 1]
if not stopped_time: if not stopped_time:
stopped_time = unix_time stopped_time = unix_time
else: else:
@ -366,9 +370,9 @@ if __name__ == "__main__":
sys.exit(1) sys.exit(1)
if ep_watched >= total_limit: if ep_watched >= total_limit:
print("{}'s limit is {} and has watched {} episodes of this show today." print("{}'s limit is {} and has watched {} episodes of this show today.".format(
.format(opts.username, total_limit, ep_watched)) opts.username, total_limit, ep_watched))
terminate_session(opts.sessionId, message, opts.notify, opts.username) terminate_session(opts.sessionId, message, opts.notify, opts.username)
else: else:
print("{}'s limit is {} but has only watched {} episodes of this show today." print("{}'s limit is {} but has only watched {} episodes of this show today.".format(
.format(opts.username, total_limit, ep_watched)) opts.username, total_limit, ep_watched))

View File

@ -20,7 +20,7 @@ Triggers: Playback Start
Arguments: Arguments:
``` ```
--jbop watch --username {username} --sessionId {session_id} --limit plays=3 --notify 1 --killMessage "You have met your limit of 3 watches." --jbop watch --username {username} --sessionId {session_id} --limit plays=3 --notify 1 --killMessage "You have met your limit of 3 watches."
``` ```
### Limit user to total Plays/Watches in a specific library (Movies) ### Limit user to total Plays/Watches in a specific library (Movies)
@ -29,7 +29,7 @@ Triggers: Playback Start
Arguments: Arguments:
``` ```
--jbop watch --username {username} --sessionId {session_id} --limit plays=3 --section Movies --killMessage "You have met your limit of 3 watches." --jbop watch --username {username} --sessionId {session_id} --limit plays=3 --section Movies --killMessage "You have met your limit of 3 watches."
``` ```
### Limit user to total time watching ### Limit user to total time watching
@ -59,4 +59,3 @@ Arguments:
``` ```
--jbop time --username {username} --sessionId {session_id} --duration {duration} --limit hours=2 --killMessage "You have met your limit of 3 days and 10 hours." --jbop time --username {username} --sessionId {session_id} --duration {duration} --limit hours=2 --killMessage "You have met your limit of 3 days and 10 hours."
``` ```

View File

@ -248,4 +248,4 @@ Add `--debug` to enable debug logging.
All examples use \[ `Transcode Decision` | `is` | `transcode` \] which will kill any variant of transcoding. 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 If you want to allow audio or container transcoding and only drop video transcodes, your condition would change to
\[ `Video Decision` | `is` | `transcode` \] \[ `Video Decision` | `is` | `transcode` \]

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Use Tautulli draw a map connecting Server to Clients based on IP addresses. Use Tautulli draw a map connecting Server to Clients based on IP addresses.
@ -30,12 +33,12 @@ import json
import os import os
from collections import OrderedDict from collections import OrderedDict
import argparse import argparse
import numpy as np # import numpy as np
import time import time
import webbrowser import webbrowser
import re import re
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = '' # Your Tautulli API key TAUTULLI_APIKEY = '' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
@ -259,7 +262,7 @@ def draw_map(map_type, geo_dict, filename, headless, leg_choice):
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap from mpl_toolkits.basemap import Basemap
## Map stuff ## # ## Map stuff ##
plt.figure(figsize=(16, 9), dpi=100, frameon=False) plt.figure(figsize=(16, 9), dpi=100, frameon=False)
lon_r = 0 lon_r = 0
lon_l = 0 lon_l = 0

View File

@ -1,4 +1,3 @@
# Maps # Maps
Maps are created with either Matplotlib/Basemap or as a geojson file on an anonymous gist. Maps are created with either Matplotlib/Basemap or as a geojson file on an anonymous gist.
@ -46,7 +45,7 @@ Choose which map type you'd like by using the `-l` argument:
- [x] Add check for user count in user_table to allow for greater than 25 users - [Pull](https://github.com/blacktwin/JBOPS/pull/3) - [x] Add check for user count in user_table to allow for greater than 25 users - [Pull](https://github.com/blacktwin/JBOPS/pull/3)
- [x] If platform is missing from PLATFORM_COLORS use DEFAULT_COLOR - [Pull](https://github.com/blacktwin/JBOPS/pull/4) - [x] If platform is missing from PLATFORM_COLORS use DEFAULT_COLOR - [Pull](https://github.com/blacktwin/JBOPS/pull/4)
- [x] Add arg to allow for runs in headless (mpl.use("Agg")) - [x] Add arg to allow for runs in headless (mpl.use("Agg"))
- [x] Add pass on N/A values for Lon/Lat - [Pull](https://github.com/blacktwin/JBOPS/pull/2) - [x] Add pass on N/A values for Lon/Lat - [Pull](https://github.com/blacktwin/JBOPS/pull/2)
### Feature updates: ### Feature updates:
@ -57,6 +56,3 @@ Choose which map type you'd like by using the `-l` argument:
- [ ] Find server's external IP, geolocation. Allow custom location to override - [ ] Find server's external IP, geolocation. Allow custom location to override
- [ ] Add arg for tracert visualization from server to client - [ ] Add arg for tracert visualization from server to client
- [ ] Animate tracert visualization? gif? - [ ] Animate tracert visualization? gif?

View File

@ -5,4 +5,4 @@
requests requests
matplotlib matplotlib
numpy numpy
basemap basemap

View File

@ -1,5 +1,7 @@
""" #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Find what was added TFRAME ago and not watched and notify admin using Tautulli. Find what was added TFRAME ago and not watched and notify admin using Tautulli.
TAUTULLI_URL + delete_media_info_cache?section_id={section_id} TAUTULLI_URL + delete_media_info_cache?section_id={section_id}
@ -12,7 +14,7 @@ import time
TFRAME = 1.577e+7 # ~ 6 months in seconds TFRAME = 1.577e+7 # ~ 6 months in seconds
TODAY = time.time() TODAY = time.time()
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = '' # Your Tautulli API key TAUTULLI_APIKEY = '' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8183/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8183/' # Your Tautulli URL
LIBRARY_NAMES = ['Movies', 'TV Shows'] # Name of libraries you want to check. LIBRARY_NAMES = ['Movies', 'TV Shows'] # Name of libraries you want to check.
@ -162,10 +164,10 @@ for library in libraries:
# Find movie rating_key. # Find movie rating_key.
show_lst += [int(lib.rating_key)] show_lst += [int(lib.rating_key)]
except Exception as e: except Exception as e:
print "Rating_key failed: {e}".format(e=e) print("Rating_key failed: {e}".format(e=e))
except Exception as e: except Exception as e:
print "Library media info failed: {e}".format(e=e) print("Library media info failed: {e}".format(e=e))
for show in show_lst: for show in show_lst:
try: try:
@ -181,7 +183,7 @@ for show in show_lst:
u" not been watched.<d/t> <dd>File location: {x.file}</dd> <br>".format(x=meta, when=added)] u" not been watched.<d/t> <dd>File location: {x.file}</dd> <br>".format(x=meta, when=added)]
except Exception as e: except Exception as e:
print "Metadata failed. Likely end of range: {e}".format(e=e) print("Metadata failed. Likely end of range: {e}".format(e=e))
if notify_lst: if notify_lst:
BODY_TEXT = """\ BODY_TEXT = """\

View File

@ -0,0 +1,368 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Send an email with what was added to Plex in the past week using Tautulli.
Email includes title (TV: Show Name: Episode Name; Movie: Movie Title), time added, image, and summary.
Uses:
notify_added_lastweek.py -t poster -d 1 -u all -i user1 user2 -s 250 100
# email all users expect user1 & user2 what was added in the last day using posters that are 250x100
notify_added_lastweek.py -t poster -d 7 -u all
# email all users what was added in the last 7 days(week) using posters that are default sized
notify_added_lastweek.py -t poster -d 7 -u all -s 1000 500
# email all users what was added in the last 7 days(week) using posters that are 1000x500
notify_added_lastweek.py -t art -d 7 -u user1
# email user1 & self what was added in the last 7 days(week) using artwork that is default sized
notify_added_lastweek.py -t art -d 7
# email self what was added in the last 7 days(week) using artwork that is default sized
"""
import requests
import sys
import time
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
import email.utils
import smtplib
import urllib
import cgi
import uuid
import argparse
# ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = '' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
LIBRARY_NAMES = ['Movies', 'TV Shows'] # Name of libraries you want to check.
# Email settings
name = '' # Your name
sender = '' # From email address
to = [sender] # Whoever you want to email [sender, 'name@example.com']
# Emails will be sent as BCC.
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
email_port = 587 # Email port (Gmail: 587)
email_username = '' # Your email username
email_password = '' # Your email password
email_subject = 'Tautulli Added Last {} day(s) Notification' # The email subject
# Default sizing for pictures
# Poster
poster_h = 205
poster_w = 100
# Artwork
art_h = 100
art_w = 205
# ## /EDIT THESE SETTINGS ##
class METAINFO(object):
def __init__(self, data=None):
d = data or {}
self.added_at = d['added_at']
self.parent_rating_key = d['parent_rating_key']
self.title = d['title']
self.rating_key = d['rating_key']
self.media_type = d['media_type']
self.grandparent_title = d['grandparent_title']
self.thumb = d['art']
self.summary = d['summary']
def get_recent(section_id, start, count):
# Get the metadata for a media item. Count matters!
payload = {'apikey': TAUTULLI_APIKEY,
'start': str(start),
'count': str(count),
'section_id': section_id,
'cmd': 'get_recently_added'}
try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json()
if response['response']['result'] == 'success':
res_data = response['response']['data']['recently_added']
return res_data
except Exception as e:
sys.stderr.write("Tautulli API 'get_recently_added' request failed: {0}.".format(e))
def get_metadata(rating_key):
# Get the metadata for a media item.
payload = {'apikey': TAUTULLI_APIKEY,
'rating_key': rating_key,
'cmd': 'get_metadata',
'media_info': True}
try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json()
if response['response']['result'] == 'success':
res_data = response['response']['data']
return METAINFO(data=res_data)
except Exception as e:
sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e))
def get_libraries_table():
# Get the data on the Tautulli libraries table.
payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_libraries_table'}
try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json()
res_data = response['response']['data']['data']
return [d['section_id'] for d in res_data if d['section_name'] in LIBRARY_NAMES]
except Exception as e:
sys.stderr.write("Tautulli API 'get_libraries_table' request failed: {0}.".format(e))
def update_library_media_info(section_id):
# Get the data on the Tautulli media info tables.
payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_library_media_info',
'section_id': section_id,
'refresh': True}
try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.status_code
if response != 200:
print(r.content)
except Exception as e:
sys.stderr.write("Tautulli API 'update_library_media_info' request failed: {0}.".format(e))
def get_pms_image_proxy(thumb):
# Gets an image from the PMS and saves it to the image cache directory.
payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'pms_image_proxy',
'img': thumb}
try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload, stream=True)
return r.url
except Exception as e:
sys.stderr.write("Tautulli API 'get_users_tables' request failed: {0}.".format(e))
def get_users():
# Get the user list from Tautulli.
payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_users'}
try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json()
res_data = response['response']['data']
return [d for d in res_data]
except Exception as e:
sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e))
def get_rating_keys(TODAY, LASTDATE):
recent_lst = []
# Get the rating_key for what was recently added
count = 25
for section_id in glt:
start = 0
while True:
# Assume all items will be returned in descending order of added_at
recent_items = get_recent(section_id, start, count)
if all([recent_items]):
start += count
for item in recent_items:
if LASTDATE <= int(item['added_at']) <= TODAY:
recent_lst.append(item['rating_key'])
continue
elif not all([recent_items]):
break
start += count
if recent_lst:
return recent_lst
sys.stderr.write("Recently Added list: {0}.".format(recent_lst))
exit()
def build_html(rating_key, height, width, pic_type):
meta = get_metadata(str(rating_key))
added = time.ctime(float(meta.added_at))
# Pull image url
thumb_url = "{}.jpeg".format(get_pms_image_proxy(meta.thumb))
if pic_type == 'poster':
thumb_url = thumb_url.replace('%2Fart%', '%2Fposter%')
image_name = "{}.jpg".format(str(rating_key))
# Saving image in current path
urllib.urlretrieve(thumb_url, image_name)
image = dict(title=meta.rating_key, path=image_name, cid=str(uuid.uuid4()))
if meta.grandparent_title == '' or meta.media_type == 'movie':
# Movies
notify = u"<dt>{x.title} ({x.rating_key}) was added {when}.</dt>" \
u"</dt> <dd> <table> <tr> <th>" \
'<img src="cid:{cid}" alt="{alt}" width="{width}" height="{height}"> </th>' \
u" <th id=t11> {x.summary} </th> </tr> </table> </dd> <br>" \
.format(
x=meta, when=added, alt=cgi.escape(meta.rating_key),
quote=True, width=width, height=height, **image)
else:
# Shows
notify = u"<dt>{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when}." \
u"</dt> <dd> <table> <tr> <th>" \
'<img src="cid:{cid}" alt="{alt}" width="{width}" height="{height}"> </th>' \
u" <th id=t11> {x.summary} </th> </tr> </table> </dd> <br>" \
.format(
x=meta, when=added, alt=cgi.escape(meta.rating_key),
quote=True, width=width, height=height, **image)
image_text = MIMEText(u'[image: {title}]'.format(**image), 'plain', 'utf-8')
return image_text, image, notify
def send_email(msg_text_lst, notify_lst, image_lst, to, days):
"""
Using info found here: http://stackoverflow.com/a/20485764/7286812
to accomplish emailing inline images
"""
msg_html = MIMEText("""\
<html>
<head>
<style>
th#t11 {{ padding: 6px; vertical-align: top; text-align: left; }}
</style>
</head>
<body>
<p>Hi!<br>
<br>Below is the list of content added to Plex's {LIBRARY_NAMES} this week.<br>
<dl>
{notify_lst}
</dl>
</p>
</body>
</html>
""".format(
notify_lst="\n".join(notify_lst).encode("utf-8"),
LIBRARY_NAMES=" & ".join(LIBRARY_NAMES),
quote=True), 'html', 'utf-8')
message = MIMEMultipart('related')
message['Subject'] = email_subject.format(days)
message['From'] = email.utils.formataddr((name, sender))
message_alternative = MIMEMultipart('alternative')
message.attach(message_alternative)
for msg_text in msg_text_lst:
message_alternative.attach(msg_text)
message_alternative.attach(msg_html)
for img in image_lst:
with open(img['path'], 'rb') as file:
message_image_lst = [MIMEImage(file.read(), name=os.path.basename(img['path']))]
for msg in message_image_lst:
message.attach(msg)
msg.add_header('Content-ID', '<{}>'.format(img['cid']))
mailserver = smtplib.SMTP(email_server, email_port)
mailserver.ehlo()
mailserver.starttls()
mailserver.ehlo()
mailserver.login(email_username, email_password)
mailserver.sendmail(sender, to, message.as_string())
mailserver.quit()
print('Email sent')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Send an email with what was added to Plex in the past week using Tautulli.")
parser.add_argument('-t', '--type', help='Metadata picture type from Plex.',
required=True, choices=['art', 'poster'])
parser.add_argument('-s', '--size', help='Metadata picture size from Plex {Height Width}.', nargs='*')
parser.add_argument('-d', '--days', help='Time frame for which to check recently added to Plex.',
required=True, type=int)
parser.add_argument('-u', '--users', help='Which users from Plex will be emailed.',
nargs='+', default='self', type=str)
parser.add_argument('-i', '--ignore', help='Which users from Plex to ignore.',
nargs='+', default='None', type=str)
opts = parser.parse_args()
TODAY = int(time.time())
LASTDATE = int(TODAY - opts.days * 24 * 60 * 60)
# Image sizing based on type or custom size
if opts.type == 'poster' and not opts.size:
height = poster_h
width = poster_w
elif opts.size:
height = opts.size[0]
width = opts.size[1]
else:
height = art_h
width = art_w
# Find the libraries from LIBRARY_NAMES
glt = [lib for lib in get_libraries_table()]
# Update media info for libraries.
[update_library_media_info(i) for i in glt]
# Gather all users email addresses
if opts.users == ['all']:
[to.append(x['email']) for x in get_users()
if x['email'] is not None and
x['email'] not in to and
x['username'] not in opts.ignore]
elif opts.users != ['all'] and opts.users != 'self':
for get_users in get_users():
for arg_users in opts.users:
if arg_users in get_users['username']:
to = to + [str(get_users['email'])]
print('Sending email(s) to {}'.format(', '.join(to)))
# Gather rating_keys on recently added media.
rating_keys_lst = get_rating_keys(TODAY, LASTDATE)
# Build html elements from rating_key
image_lst = []
msg_text_lst = []
notify_lst = []
build_parts = [build_html(rating_key, height, width, opts.type) for rating_key in sorted(rating_keys_lst)]
for parts in build_parts:
msg_text_lst.append(parts[0])
image_lst.append(parts[1])
notify_lst.append(parts[2])
# Send email
send_email(msg_text_lst, notify_lst, image_lst, to, opts.days)
# Delete images in current path
for img in image_lst:
os.remove(img['path'])

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Delay Notification Agent message for concurrent streams Delay Notification Agent message for concurrent streams
@ -19,7 +22,7 @@ import sys
import argparse import argparse
from time import sleep from time import sleep
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = '' # Your Tautulli API key TAUTULLI_APIKEY = '' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
CONCURRENT_TOTAL = 2 CONCURRENT_TOTAL = 2
@ -44,7 +47,7 @@ BODY_TEXT = """\
def get_activity(): def get_activity():
# Get the current activity on the PMS. """Get the current activity on the PMS."""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_activity'} 'cmd': 'get_activity'}
@ -60,7 +63,7 @@ def get_activity():
def send_notification(subject_text, body_text): def send_notification(subject_text, body_text):
# Format notification text """Format notification text."""
try: try:
subject = subject_text.format(p=p, total=cc_total) subject = subject_text.format(p=p, total=cc_total)
body = body_text.format(p=p, total=cc_total, time=TIMEOUT / 60) body = body_text.format(p=p, total=cc_total, time=TIMEOUT / 60)

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Notify users of recently added episode to show that they have watched at least LIMIT times via email. Notify users of recently added episode to show that they have watched at least LIMIT times via email.
Also notify users of new movies. Also notify users of new movies.
@ -22,11 +25,11 @@ import smtplib
import sys import sys
import argparse import argparse
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
IGNORE_LST = ['123456', '123456'] # User_ids IGNORE_LST = ['123456', '123456'] # User_ids
LIMIT = 3 LIMIT = 3
# Email settings # Email settings
@ -74,6 +77,7 @@ TV_BODY = """\
user_dict = {} user_dict = {}
class Users(object): class Users(object):
def __init__(self, data=None): def __init__(self, data=None):
d = data or {} d = data or {}
@ -108,6 +112,7 @@ def get_user(user_id):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e))
def get_users(): def get_users():
# Get the user list from Tautulli. # Get the user list from Tautulli.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -134,8 +139,9 @@ def get_history(showkey):
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json() response = r.json()
res_data = response['response']['data']['data'] res_data = response['response']['data']['data']
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1 return [UserHIS(data=d) for d in res_data
and d['media_type'].lower() in ('episode', 'show')] if d['watched_status'] == 1 and
d['media_type'].lower() in ('episode', 'show')]
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
@ -177,7 +183,7 @@ def get_email(show):
def send_email(to, email_subject, body_html): def send_email(to, email_subject, body_html):
### Do not edit below ### # ## Do not edit below ###
message = MIMEText(body_html, 'html') message = MIMEText(body_html, 'html')
message['Subject'] = email_subject message['Subject'] = email_subject
message['From'] = email.utils.formataddr((name, sender)) message['From'] = email.utils.formataddr((name, sender))
@ -188,7 +194,7 @@ def send_email(to, email_subject, body_html):
mailserver.login(email_username, email_password) mailserver.login(email_username, email_password)
mailserver.sendmail(sender, to, message.as_string()) mailserver.sendmail(sender, to, message.as_string())
mailserver.quit() mailserver.quit()
print 'Email sent' print('Email sent')
if __name__ == '__main__': if __name__ == '__main__':
@ -241,7 +247,7 @@ if __name__ == '__main__':
body_html = MOVIE_BODY.format(p=p) body_html = MOVIE_BODY.format(p=p)
send_email(to, email_subject, body_html) send_email(to, email_subject, body_html)
elif p.media_type in ['show', 'season', 'episode']: elif p.media_type in ['show', 'season', 'episode']:
email_subject = TV_SUBJECT.format(p=p) email_subject = TV_SUBJECT.format(p=p)
to = get_email(int(p.grandparent_rating_key)) to = get_email(int(p.grandparent_rating_key))
body_html = TV_BODY.format(p=p) body_html = TV_BODY.format(p=p)

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Pulling together User IP information and Email. Pulling together User IP information and Email.
@ -19,7 +21,7 @@ import argparse
import requests import requests
import sys import sys
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = '' # Your Tautulli API key TAUTULLI_APIKEY = '' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
NOTIFIER_ID = 12 # The notification notifier ID NOTIFIER_ID = 12 # The notification notifier ID
@ -39,9 +41,9 @@ BODY_TEXT = """\
<head></head> <head></head>
<body> <body>
<p>Hi!<br> <p>Hi!<br>
<br><a href="mailto:{u.email}"><img src="{u.user_thumb}" alt="Poster unavailable" height="50" width="50"></a> <br><a href="mailto:{u.email}"><img src="{u.user_thumb}" alt="Poster unavailable" height="50" width="50"></a>
{p.user} has watched {p.media_type}:{p.title} from a new IP address: {p.ip_address}<br> {p.user} has watched {p.media_type}:{p.title} from a new IP address: {p.ip_address}<br>
<br>On {p.platform}[{p.player}] in <br>On {p.platform}[{p.player}] in
<a href="http://maps.google.com/?q={g.city},{g.country},{g.postal_code}">{g.city}, {g.country} {g.postal_code}</a> <a href="http://maps.google.com/?q={g.city},{g.country},{g.postal_code}">{g.city}, {g.country} {g.postal_code}</a>
at {p.timestamp} on {p.datestamp}<br> at {p.timestamp} on {p.datestamp}<br>
<br><br> <br><br>
@ -69,7 +71,7 @@ class UserEmail(object):
def get_user_ip_addresses(user_id='', ip_address=''): def get_user_ip_addresses(user_id='', ip_address=''):
# Get the user IP list from Tautulli """Get the user IP list from Tautulli."""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_user_ips', 'cmd': 'get_user_ips',
'user_id': user_id, 'user_id': user_id,
@ -99,7 +101,7 @@ def get_user_ip_addresses(user_id='', ip_address=''):
def get_geoip_info(ip_address=''): def get_geoip_info(ip_address=''):
# Get the geo IP lookup from Tautulli """Get the geo IP lookup from Tautulli."""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_geoip_lookup', 'cmd': 'get_geoip_lookup',
'ip_address': ip_address} 'ip_address': ip_address}
@ -123,7 +125,7 @@ def get_geoip_info(ip_address=''):
def get_user_email(user_id=''): def get_user_email(user_id=''):
# Get the user email from Tautulli """Get the user email from Tautulli."""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_user', 'cmd': 'get_user',
'user_id': user_id} 'user_id': user_id}
@ -147,7 +149,7 @@ def get_user_email(user_id=''):
def send_notification(arguments=None, geodata=None, useremail=None): def send_notification(arguments=None, geodata=None, useremail=None):
# Format notification text """Format notification text."""
try: try:
subject = SUBJECT_TEXT.format(p=arguments, g=geodata, u=useremail) subject = SUBJECT_TEXT.format(p=arguments, g=geodata, u=useremail)
body = BODY_TEXT.format(p=arguments, g=geodata, u=useremail) body = BODY_TEXT.format(p=arguments, g=geodata, u=useremail)

103
notify/notify_on_added.py Normal file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tautulli > Settings > Notification Agents > Scripts > Bell icon:
[X] Notify on Recently Added
Tautulli > Settings > Notification Agents > Scripts > Gear icon:
Recently Added: notify_on_added.py
Tautulli > Settings > Notifications > Script > Script Arguments:
-sn {show_name} -ena {episode_name} -ssn {season_num00} -enu {episode_num00} -srv {server_name} -med {media_type} -pos {poster_url} -tt {title} -sum {summary} -lbn {library_name}
You can add more arguments if you want more details in the email body
"""
from email.mime.text import MIMEText
import email.utils
import smtplib
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-sn', '--show_name', action='store', default='',
help='The name of the TV show')
parser.add_argument('-ena', '--episode_name', action='store', default='',
help='The name of the episode')
parser.add_argument('-ssn', '--season_num', action='store', default='',
help='The season number of the TV show')
parser.add_argument('-enu', '--episode_num', action='store', default='',
help='The episode number of the TV show')
parser.add_argument('-srv', '--plex_server', action='store', default='',
help='The name of the Plex server')
parser.add_argument('-med', '--show_type', action='store', default='',
help='The type of media')
parser.add_argument('-pos', '--poster', action='store', default='',
help='The poster url')
parser.add_argument('-tt', '--title', action='store', default='',
help='The title of the TV show')
parser.add_argument('-sum', '--summary', action='store', default='',
help='The summary of the TV show')
parser.add_argument('-lbn', '--library_name', action='store', default='',
help='The name of the TV show')
p = parser.parse_args()
# Edit user@email.com and shows
users = [{'email': 'user1@gmail.com',
'shows': ('show1', 'show2')
},
{'email': 'user2@gmail.com',
'shows': ('show1', 'show2', 'show3')
},
{'email': 'user3@gmail.com',
'shows': ('show1', 'show2', 'show3', 'show4')
}]
# Kill script now if show_name is not in lists
too = list('Match' for u in users if p.show_name in u['shows'])
if not too:
print('Kill script now show_name is not in lists')
exit()
# Join email addresses
to = list([u['email'] for u in users if p.show_name in u['shows']])
# Email settings
name = 'Tautulli' # Your name
sender = 'sender' # From email address
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
email_port = 587 # Email port (Gmail: 587)
email_username = 'email' # Your email username
email_password = 'password' # Your email password
email_subject = 'New episode for ' + p.show_name + ' is available on ' + p.plex_server # The email subject
# Detailed body for tv shows
show_html = """\
<html>
<head></head>
<body>
<p>Hi!<br>
{p.show_name} S{p.season_num} - E{p.episode_num} -- {p.episode_name} -- was recently added to {p.library_name} on PLEX
<br><br>
<br> {p.summary} <br>
<br><img src="{p.poster}" alt="Poster unavailable" height="150" width="102"><br>
</p>
</body>
</html>
""".format(p=p)
# ### Do not edit below ###
# Check to see whether it is a tv show
if p.show_type.lower() == 'show' or p.show_type.lower() == 'episode':
message = MIMEText(show_html, 'html')
message['Subject'] = email_subject
message['From'] = email.utils.formataddr((name, sender))
mailserver = smtplib.SMTP(email_server, email_port)
mailserver.starttls()
mailserver.ehlo()
mailserver.login(email_username, email_password)
mailserver.sendmail(sender, to, message.as_string())
mailserver.quit()
else:
exit()

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Description: Notify only if recently aired/released Description: Notify only if recently aired/released
Author: Blacktwin Author: Blacktwin
@ -64,6 +67,7 @@ aired_date = datetime.strptime(air_date, date_format)
today = date.today() today = date.today()
delta = today - aired_date.date() delta = today - aired_date.date()
def notify_recently_added(rating_key, notifier_id): def notify_recently_added(rating_key, notifier_id):
# Get the metadata for a media item. # Get the metadata for a media item.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -79,8 +83,9 @@ def notify_recently_added(rating_key, notifier_id):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'notify_recently_added' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'notify_recently_added' request failed: {0}.".format(e))
pass pass
if delta.days < RECENT_DAYS: if delta.days < RECENT_DAYS:
notify_recently_added(rating_key, NOTIFIER_ID) notify_recently_added(rating_key, NOTIFIER_ID)
else: else:
print("Not recent enough, no notification to be sent.") print("Not recent enough, no notification to be sent.")

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Notify users of recently added episode to show that they have watched at least LIMIT times via email. Notify users of recently added episode to show that they have watched at least LIMIT times via email.
Block users with IGNORE_LST. Block users with IGNORE_LST.
@ -21,11 +24,11 @@ import smtplib
import sys import sys
import argparse import argparse
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
IGNORE_LST = [123456, 123456] # User_ids IGNORE_LST = [123456, 123456] # User_ids
LIMIT = 3 LIMIT = 3
# Email settings # Email settings
@ -38,6 +41,7 @@ email_password = '' # Your email password
user_dict = {} user_dict = {}
class Users(object): class Users(object):
def __init__(self, data=None): def __init__(self, data=None):
d = data or {} d = data or {}
@ -74,7 +78,10 @@ def get_user(user_id):
def get_history(showkey): def get_history(showkey):
# Get the user history from Tautulli. Length matters! """Get the user history from Tautulli.
Length matters!
"""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_history', 'cmd': 'get_history',
'grandparent_rating_key': showkey, 'grandparent_rating_key': showkey,
@ -84,8 +91,9 @@ def get_history(showkey):
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json() response = r.json()
res_data = response['response']['data']['data'] res_data = response['response']['data']['data']
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1 return [UserHIS(data=d) for d in res_data
and d['media_type'].lower() in ('episode', 'show')] if d['watched_status'] == 1 and
d['media_type'].lower() in ('episode', 'show')]
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
@ -188,7 +196,7 @@ if __name__ == '__main__':
</html> </html>
""".format(p=p) """.format(p=p)
### Do not edit below ### # ## Do not edit below ###
message = MIMEText(show_html, 'html') message = MIMEText(show_html, 'html')
message['Subject'] = email_subject message['Subject'] = email_subject
message['From'] = email.utils.formataddr((name, sender)) message['From'] = email.utils.formataddr((name, sender))
@ -199,4 +207,4 @@ if __name__ == '__main__':
mailserver.login(email_username, email_password) mailserver.login(email_username, email_password)
mailserver.sendmail(sender, to, message.as_string()) mailserver.sendmail(sender, to, message.as_string())
mailserver.quit() mailserver.quit()
print 'Email sent' print('Email sent')

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Pulling together User IP information and Email. Pulling together User IP information and Email.
Enable the API under Settings > Access Control and remember your API key. Enable the API under Settings > Access Control and remember your API key.
@ -13,9 +16,9 @@ from email.mime.text import MIMEText
import email.utils import email.utils
import smtplib import smtplib
## -sn {show_name} -ena {episode_name} -ssn {season_num00} -enu {episode_num00} -srv {server_name} -med {media_type} -pos {poster_url} -tt {title} -sum {summary} -lbn {library_name} -ip {ip_address} -us {user} -uid {user_id} -pf {platform} -pl {player} -da {datestamp} -ti {timestamp} # ## -sn {show_name} -ena {episode_name} -ssn {season_num00} -enu {episode_num00} -srv {server_name} -med {media_type} -pos {poster_url} -tt {title} -sum {summary} -lbn {library_name} -ip {ip_address} -us {user} -uid {user_id} -pf {platform} -pl {player} -da {datestamp} -ti {timestamp}
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'xxxxxxxxxxx' # Your Tautulli API key TAUTULLI_APIKEY = 'xxxxxxxxxxx' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
@ -44,17 +47,20 @@ BODY_TEXT = """\
""" """
# Email settings # Email settings
name = '' # Your name name = '' # Your name
sender = '' # From email address sender = '' # From email address
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com) email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
email_port = 587 # Email port (Gmail: 587) email_port = 587 # Email port (Gmail: 587)
email_username = '' # Your email username email_username = '' # Your email username
email_password = '' # Your email password email_password = '' # Your email password
email_subject = "New IP has been detected using Plex." email_subject = "New IP has been detected using Plex."
IGNORE_LST = ['123456', '123456'] # User_id IGNORE_LST = ['123456', '123456'] # User_id
# ##Geo Space##
##Geo Space##
class GeoData(object): class GeoData(object):
def __init__(self, data=None): def __init__(self, data=None):
data = data or {} data = data or {}
@ -62,22 +68,28 @@ class GeoData(object):
self.city = data.get('city', 'N/A') self.city = data.get('city', 'N/A')
self.postal_code = data.get('postal_code', 'N/A') self.postal_code = data.get('postal_code', 'N/A')
##USER Space##
# ##USER Space##
class UserEmail(object): class UserEmail(object):
def __init__(self, data=None): def __init__(self, data=None):
data = data or {} data = data or {}
self.email = data.get('email', 'N/A') self.email = data.get('email', 'N/A')
self.user_id = data.get('user_id', 'N/A') self.user_id = data.get('user_id', 'N/A')
self.user_thumb = data.get('user_thumb', 'N/A') self.user_thumb = data.get('user_thumb', 'N/A')
##API Space##
# ##API Space##
def get_user_ip_addresses(user_id='', ip_address=''): def get_user_ip_addresses(user_id='', ip_address=''):
# Get the user IP list from Tautulli # Get the user IP list from Tautulli
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_user_ips', 'cmd': 'get_user_ips',
'user_id': user_id, 'user_id': user_id,
'search': ip_address} 'search': ip_address}
try: try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json() response = r.json()
@ -99,6 +111,7 @@ def get_user_ip_addresses(user_id='', ip_address=''):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_user_ip_addresses' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_user_ip_addresses' request failed: {0}.".format(e))
def get_geoip_info(ip_address=''): def get_geoip_info(ip_address=''):
# Get the geo IP lookup from Tautulli # Get the geo IP lookup from Tautulli
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -146,6 +159,7 @@ def get_user_email(user_id=''):
sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e))
return UserEmail() return UserEmail()
def send_notification(arguments=None, geodata=None, useremail=None): def send_notification(arguments=None, geodata=None, useremail=None):
# Format notification text # Format notification text
try: try:
@ -163,12 +177,12 @@ def send_notification(arguments=None, geodata=None, useremail=None):
mailserver.login(email_username, email_password) mailserver.login(email_username, email_password)
mailserver.sendmail(sender, u.email, message.as_string()) mailserver.sendmail(sender, u.email, message.as_string())
mailserver.quit() mailserver.quit()
print 'Email sent' print('Email sent')
except Exception as e: except Exception as e:
sys.stderr.write("Email Failure: {0}.".format(e)) sys.stderr.write("Email Failure: {0}.".format(e))
def clr_sql(ip):
def clr_sql(ip):
try: try:
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'sql', 'cmd': 'sql',
@ -179,6 +193,7 @@ def clr_sql(ip):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_sql' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_sql' request failed: {0}.".format(e))
if __name__ == '__main__': if __name__ == '__main__':
# Parse arguments from Tautulli # Parse arguments from Tautulli
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -188,7 +203,7 @@ if __name__ == '__main__':
parser.add_argument('-us', '--user', action='store', default='', parser.add_argument('-us', '--user', action='store', default='',
help='Username of the person watching the stream') help='Username of the person watching the stream')
parser.add_argument('-uid', '--user_id', action='store', default='', parser.add_argument('-uid', '--user_id', action='store', default='',
help='User_ID of the person watching the stream') help='User_ID of the person watching the stream')
parser.add_argument('-med', '--media_type', action='store', default='', parser.add_argument('-med', '--media_type', action='store', default='',
help='The media type of the stream') help='The media type of the stream')
parser.add_argument('-tt', '--title', action='store', default='', parser.add_argument('-tt', '--title', action='store', default='',
@ -217,7 +232,7 @@ if __name__ == '__main__':
help='The summary of the TV show') help='The summary of the TV show')
parser.add_argument('-lbn', '--library_name', action='store', default='', parser.add_argument('-lbn', '--library_name', action='store', default='',
help='The name of the TV show') help='The name of the TV show')
p = parser.parse_args() p = parser.parse_args()
if p.user_id not in IGNORE_LST: if p.user_id not in IGNORE_LST:

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
1. Install the requests module for python. 1. Install the requests module for python.
pip install requests pip install requests
@ -14,18 +17,18 @@ Tautulli > Settings > Notifications > Script > Script Arguments:
https://gist.github.com/blacktwin/261c416dbed08291e6d12f6987d9bafa https://gist.github.com/blacktwin/261c416dbed08291e6d12f6987d9bafa
""" """
from twitter import * from twitter import Twitter, OAuth
import argparse import argparse
import requests import requests
import os import os
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TOKEN = '' TOKEN = ''
TOKEN_SECRET = '' TOKEN_SECRET = ''
CONSUMER_KEY = '' CONSUMER_KEY = ''
CONSUMER_SECRET = '' CONSUMER_SECRET = ''
TITLE_FIND = ['Friends'] # Title to ignore ['Snow White'] TITLE_FIND = ['Friends'] # Title to ignore ['Snow White']
TWITTER_USER = ' @username' TWITTER_USER = ' @username'
BODY_TEXT = '' BODY_TEXT = ''
@ -80,12 +83,13 @@ if __name__ == '__main__':
p = parser.parse_args() p = parser.parse_args()
if p.media_type == 'movie': if p.media_type == 'movie':
BODY_TEXT = MOVIE_TEXT.format(media_type=p.media_type, title=p.title, duration=p.duration) BODY_TEXT = MOVIE_TEXT.format(media_type=p.media_type, title=p.title, duration=p.duration)
elif p.media_type == 'episode': elif p.media_type == 'episode':
BODY_TEXT = TV_TEXT.format(media_type=p.media_type, show_name=p.show_name, title=p.title, BODY_TEXT = TV_TEXT.format(
season_num00=p.season_num, episode_num00=p.episode_num, duration=p.duration) media_type=p.media_type, show_name=p.show_name, title=p.title,
season_num00=p.season_num, episode_num00=p.episode_num,
duration=p.duration)
else: else:
exit() exit()
@ -101,7 +105,6 @@ if __name__ == '__main__':
for chunk in request: for chunk in request:
image.write(chunk) image.write(chunk)
t_upload = Twitter(domain='upload.twitter.com', t_upload = Twitter(domain='upload.twitter.com',
auth=OAuth(TOKEN, TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET)) auth=OAuth(TOKEN, TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET))

View File

@ -1,7 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Find when media was added between STARTFRAME and ENDFRAME to Plex through Tautulli. Find when media was added between STARTFRAME and ENDFRAME to Plex through Tautulli.
Some Exceptions have been commented out to supress what is printed. Some Exceptions have been commented out to supress what is printed.
Uncomment Exceptions if you run into problem and need to investigate. Uncomment Exceptions if you run into problem and need to investigate.
""" """
@ -9,22 +12,20 @@ import requests
import sys import sys
import time import time
STARTFRAME = 1480550400 # 2016, Dec 1 in seconds
STARTFRAME = 1480550400 # 2016, Dec 1 in seconds ENDFRAME = 1488326400 # 2017, March 1 in seconds
ENDFRAME = 1488326400 # 2017, March 1 in seconds
TODAY = int(time.time()) TODAY = int(time.time())
LASTMONTH = int(TODAY - 2629743) # 2629743 = 1 month in seconds LASTMONTH = int(TODAY - 2629743) # 2629743 = 1 month in seconds
# Uncomment to change range to 1 month ago - Today # Uncomment to change range to 1 month ago - Today
# STARTFRAME = LASTMONTH # STARTFRAME = LASTMONTH
# ENDFRAME = TODAY # ENDFRAME = TODAY
# ## EDIT THESE SETTINGS ##
## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
LIBRARY_NAMES = ['TV Shows', 'Movies'] # Names of your libraries you want to check. LIBRARY_NAMES = ['TV Shows', 'Movies'] # Names of your libraries you want to check.
class LIBINFO(object): class LIBINFO(object):
@ -70,6 +71,7 @@ def get_new_rating_keys(rating_key, media_type):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_new_rating_keys' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_new_rating_keys' request failed: {0}.".format(e))
def get_library_media_info(section_id): def get_library_media_info(section_id):
# Get the data on the Tautulli media info tables. Length matters! # Get the data on the Tautulli media info tables. Length matters!
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -88,6 +90,7 @@ def get_library_media_info(section_id):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e))
def get_metadata(rating_key): def get_metadata(rating_key):
# Get the metadata for a media item. # Get the metadata for a media item.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -106,6 +109,7 @@ def get_metadata(rating_key):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e))
def update_library_media_info(section_id): def update_library_media_info(section_id):
# Get the data on the Tautulli media info tables. # Get the data on the Tautulli media info tables.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -122,6 +126,7 @@ def update_library_media_info(section_id):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'update_library_media_info' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'update_library_media_info' request failed: {0}.".format(e))
def get_libraries_table(): def get_libraries_table():
# Get the data on the Tautulli libraries table. # Get the data on the Tautulli libraries table.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -181,15 +186,16 @@ for i in sorted(show_lst, reverse=True):
# Shows # Shows
print(u"{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when}.".format(x=x, when=added)) print(u"{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when}.".format(x=x, when=added))
except Exception as e: except Exception:
# Remove commented print below to investigate problems. # Remove commented print below to investigate problems.
# print("Metadata failed. Likely end of range: {e}").format(e=e) # print("Metadata failed. Likely end of range: {e}").format(e=e)
# Remove break if not finding files in range # Remove break if not finding files in range
break break
print("There were {amount} files added between {start}:{end}".format(amount=len(count_lst), print("There were {amount} files added between {start}:{end}".format(
start=time.ctime(float(STARTFRAME)), amount=len(count_lst),
end=time.ctime(float(ENDFRAME)))) start=time.ctime(float(STARTFRAME)),
end=time.ctime(float(ENDFRAME))))
print("Total movies: {}".format(count_lst.count('movie'))) print("Total movies: {}".format(count_lst.count('movie')))
print("Total shows: {}".format(count_lst.count('show') + count_lst.count('episode'))) print("Total shows: {}".format(count_lst.count('show') + count_lst.count('episode')))
print("Total size of files added: {}MB".format(sum(size_lst)>>20)) print("Total size of files added: {}MB".format(sum(size_lst) >> 20))

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 1. Install the requests module for python. # 1. Install the requests module for python.
# pip install requests # pip install requests
# 2. Add script arguments in Tautulli. # 2. Add script arguments in Tautulli.
@ -10,7 +13,7 @@ import sys
user = sys.argv[1] user = sys.argv[1]
title = sys.argv[2] title = sys.argv[2]
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXXXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
NOTIFIER_ID = 10 # The notification notifier ID for Tautulli NOTIFIER_ID = 10 # The notification notifier ID for Tautulli
@ -25,7 +28,7 @@ BODY_TEXT = """\
</p> </p>
</body> </body>
</html> </html>
""" %(user, title) """ % (user, title)
class UserHIS(object): class UserHIS(object):
@ -33,9 +36,9 @@ class UserHIS(object):
data = data or {} data = data or {}
self.watched = [d['watched_status'] for d in data] self.watched = [d['watched_status'] for d in data]
def get_history(): def get_history():
# Get the user IP list from Tautulli """Get the history from Tautulli."""
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_history', 'cmd': 'get_history',
'user': user, 'user': user,

View File

@ -1,19 +1,23 @@
''' #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Run script by itself. Will look for WARN code followed by /library/metadata/ str in Plex logs. Run script by itself. Will look for WARN code followed by /library/metadata/ str in Plex logs.
This is find files that are corrupt or having playback issues. This is find files that are corrupt or having playback issues.
I corrupted a file to test. I corrupted a file to test.
''' """
import requests import requests
import sys import sys
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
lib_met = [] lib_met = []
err_title = [] err_title = []
class PlexLOG(object): class PlexLOG(object):
def __init__(self, data=None): def __init__(self, data=None):
self.error_msg = [] self.error_msg = []
@ -43,6 +47,7 @@ def get_plex_log():
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_plex_log' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_plex_log' request failed: {0}.".format(e))
def get_history(key): def get_history(key):
# Get the user IP list from Tautulli # Get the user IP list from Tautulli
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -59,6 +64,7 @@ def get_history(key):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
if __name__ == '__main__': if __name__ == '__main__':
p_log = get_plex_log() p_log = get_plex_log()
for co, msg in p_log.error_msg: for co, msg in p_log.error_msg:

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import psutil import psutil
import requests import requests
@ -6,20 +9,21 @@ drive = 'F:'
disk = psutil.disk_partitions() disk = psutil.disk_partitions()
TAUTULLI_URL = 'http://localhost:8182/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8182/' # Your Tautulli URL
TAUTULLI_APIKEY = 'xxxxxx' # Enter your Tautulli API Key TAUTULLI_APIKEY = 'xxxxxx' # Enter your Tautulli API Key
NOTIFIER_LST = [10, 11] # The Tautulli notifier notifier id found here: https://github.com/drzoidberg33/plexpy/blob/master/plexpy/notifiers.py#L43 NOTIFIER_LST = [10, 11] # The Tautulli notifier notifier id found here: https://github.com/drzoidberg33/plexpy/blob/master/plexpy/notifiers.py#L43
NOTIFY_SUBJECT = 'Tautulli' # The notification subject NOTIFY_SUBJECT = 'Tautulli' # The notification subject
NOTIFY_BODY = 'The Plex disk {0} was not found'.format(drive) # The notification body NOTIFY_BODY = 'The Plex disk {0} was not found'.format(drive) # The notification body
disk_check = [True for i in disk if drive in i.mountpoint] disk_check = [True for i in disk if drive in i.mountpoint]
if not disk_check: if not disk_check:
# Send the notification through Tautulli # Send the notification through Tautulli
payload = {'apikey': TAUTULLI_APIKEY, payload = {
'cmd': 'notify', 'apikey': TAUTULLI_APIKEY,
'subject': NOTIFY_SUBJECT, 'cmd': 'notify',
'body': NOTIFY_BODY} 'subject': NOTIFY_SUBJECT,
'body': NOTIFY_BODY}
for notifier in NOTIFIER_LST: for notifier in NOTIFIER_LST:
payload['notifier_id'] = notifier payload['notifier_id'] = notifier

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Use Tautulli to print plays by library from 0, 1, 7, or 30 days ago. 0 = total Use Tautulli to print plays by library from 0, 1, 7, or 30 days ago. 0 = total
@ -26,14 +29,15 @@ import requests
import sys import sys
import argparse import argparse
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
OUTPUT = 'Library: {section}\nDays: {days}\nPlays: {plays}' OUTPUT = 'Library: {section}\nDays: {days}\nPlays: {plays}'
## CODE BELOW ## # ## CODE BELOW ##
def get_library_names(): def get_library_names():
# Get a list of new rating keys for the PMS of all of the item's parent/children. # Get a list of new rating keys for the PMS of all of the item's parent/children.

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Use Tautulli to pull plays by library Use Tautulli to pull plays by library
@ -6,8 +9,8 @@ optional arguments:
-l [ ...], --libraries [ ...] -l [ ...], --libraries [ ...]
Space separated list of case sensitive names to process. Allowed names are: Space separated list of case sensitive names to process. Allowed names are:
(choices: All Library Names) (choices: All Library Names)
Usage: Usage:
plays_by_library.py -l "TV Shows" Movies plays_by_library.py -l "TV Shows" Movies
TV Shows - Plays: 2859 TV Shows - Plays: 2859
@ -18,18 +21,17 @@ Usage:
import requests import requests
import sys import sys
import argparse import argparse
import json # import json
# ## EDIT THESE SETTINGS ##
## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'xxxxxx' # Your Tautulli API key TAUTULLI_APIKEY = 'xxxxxx' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
OUTPUT = '{section} - Plays: {plays}' OUTPUT = '{section} - Plays: {plays}'
## CODE BELOW ## # ## CODE BELOW ##
def get_libraries_table(sections=None): def get_libraries_table(sections=None):
# Get a list of new rating keys for the PMS of all of the item's parent/children. # Get a list of new rating keys for the PMS of all of the item's parent/children.

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
usage: plex_netflix_check.py [-h] [-l [...]] [-s ] [-t ] usage: plex_netflix_check.py [-h] [-l [...]] [-s ] [-t ]
@ -29,15 +32,14 @@ import argparse
from xmljson import badgerfish as bf from xmljson import badgerfish as bf
from lxml.html import fromstring from lxml.html import fromstring
from time import sleep from time import sleep
import json
from plexapi.server import PlexServer from plexapi.server import PlexServer
# pip install plexapi # pip install plexapi
## Edit ## # ## Edit ##
PLEX_URL = 'http://localhost:32400' PLEX_URL = 'http://localhost:32400'
PLEX_TOKEN = 'xxxx' PLEX_TOKEN = 'xxxx'
## /Edit ## # ## /Edit ##
sess = requests.Session() sess = requests.Session()
sess.verify = False sess.verify = False
@ -57,7 +59,7 @@ def instantwatch_search(name, media_type, site, search_limit):
elif media_type == 'episode': elif media_type == 'episode':
content_type = '4' content_type = '4'
else: else:
content_type ='' content_type = ''
payload = {'content_type': content_type, payload = {'content_type': content_type,
'q': name.lower()} 'q': name.lower()}
@ -108,7 +110,7 @@ def instantwatch_search(name, media_type, site, search_limit):
pass pass
for data in results['span']: for data in results['span']:
if data['@class'] == 'title' and search_limit is not 0: if data['@class'] == 'title' and search_limit != 0:
if str(data['a']['$']).lower().startswith(name.lower()): if str(data['a']['$']).lower().startswith(name.lower()):
if amazon_id: if amazon_id:
if data['a']['@data-title-id'] == amazon_id: if data['a']['@data-title-id'] == amazon_id:
@ -119,10 +121,10 @@ def instantwatch_search(name, media_type, site, search_limit):
print('Page: {}{}'.format(NETFLIX_URL, data['a']['@data-title-id'])) print('Page: {}{}'.format(NETFLIX_URL, data['a']['@data-title-id']))
results_count += 1 results_count += 1
search_limit -= 1 search_limit -= 1
if search_limit is 0: if search_limit == 0:
limit = True limit = True
elif data['@class'] == 'title' and search_limit is 0 and limit is False: elif data['@class'] == 'title' and search_limit == 0 and limit is False:
if data['a']['$'].lower().startswith(name.lower()): if data['a']['$'].lower().startswith(name.lower()):
if amazon_id: if amazon_id:
if data['a']['@data-title-id'] == amazon_id: if data['a']['@data-title-id'] == amazon_id:
@ -206,7 +208,7 @@ def main():
'(choices: %(choices)s)\n(default: %(default)s)') '(choices: %(choices)s)\n(default: %(default)s)')
parser.add_argument('-site', '--site', metavar='', choices=['Netflix', 'Amazon', 'Both'], nargs='?', parser.add_argument('-site', '--site', metavar='', choices=['Netflix', 'Amazon', 'Both'], nargs='?',
default='Both', help='Refine search for name by using type.\n' default='Both', help='Refine search for name by using type.\n'
'(choices: %(choices)s)\n(default: %(default)s)') '(choices: %(choices)s)\n(default: %(default)s)')
parser.add_argument('-sl', '--search_limit', metavar='', nargs='?', type=int, default=5, parser.add_argument('-sl', '--search_limit', metavar='', nargs='?', type=int, default=5,
help='Define number of search returns from page. Zero returns all.' help='Define number of search returns from page. Zero returns all.'
'\n(default: %(default)s)') '\n(default: %(default)s)')
@ -223,5 +225,6 @@ def main():
else: else:
plex_library_search(opts.library[0], opts.site, opts.episodes, opts.search_limit) plex_library_search(opts.library[0], opts.site, opts.episodes, opts.search_limit)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Description: Comparing content between two or more Plex servers. Description: Comparing content between two or more Plex servers.
Creates .json file in script directory of server compared. Creates .json file in script directory of server compared.
@ -103,14 +105,14 @@ def get_meta(meta):
""" """
thumb_url = '{}{}?X-Plex-Token={}'.format( thumb_url = '{}{}?X-Plex-Token={}'.format(
meta._server._baseurl, meta.thumb, meta._server._token) meta._server._baseurl, meta.thumb, meta._server._token)
meta_dict = {'title': meta.title, meta_dict = {'title': meta.title,
'rating': meta.rating if 'rating': meta.rating if
meta.rating != None else 0.0, meta.rating is not None else 0.0,
'genres': [x.tag for x in meta.genres], 'genres': [x.tag for x in meta.genres],
'server': [meta._server.friendlyName], 'server': [meta._server.friendlyName],
'thumb': [thumb_url] 'thumb': [thumb_url]
} }
if meta.guid: if meta.guid:
# guid will return (com.plexapp.agents.imdb://tt4302938?lang=en) # guid will return (com.plexapp.agents.imdb://tt4302938?lang=en)
# Agents will differ between servers. # Agents will differ between servers.
@ -193,8 +195,9 @@ def org_diff(lst_dicts, media_type, main_server):
# Sort item list by Plex rating # Sort item list by Plex rating
# Duplicates will use originals rating # Duplicates will use originals rating
meta_lst = sorted(meta_lst, key=lambda d: d['rating'], reverse=True) meta_lst = sorted(meta_lst, key=lambda d: d['rating'], reverse=True)
diff_dict[mtype] = {'combined': {'count': len(meta_lst), diff_dict[mtype] = {'combined': {
'list': meta_lst}} 'count': len(meta_lst),
'list': meta_lst}}
print('...finding {}s missing from {}'.format( print('...finding {}s missing from {}'.format(
mtype, main_server)) mtype, main_server))
@ -205,13 +208,15 @@ def org_diff(lst_dicts, media_type, main_server):
# Main Server name is absent in items server list # Main Server name is absent in items server list
elif main_server in item['server'] and len(item['server']) == 1: elif main_server in item['server'] and len(item['server']) == 1:
unique.append(item) unique.append(item)
diff_dict[mtype].update({'missing': {'count': len(missing), diff_dict[mtype].update({'missing': {
'list': missing}}) 'count': len(missing),
'list': missing}})
print('...finding {}s unique to {}'.format( print('...finding {}s unique to {}'.format(
mtype, main_server)) mtype, main_server))
diff_dict[mtype].update({'unique': {'count': len(unique), diff_dict[mtype].update({'unique': {
'list': unique}}) 'count': len(unique),
'list': unique}})
return diff_dict return diff_dict
@ -220,7 +225,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Comparing content between two or more Plex servers.", description="Comparing content between two or more Plex servers.",
formatter_class = argparse.RawTextHelpFormatter) formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--server', required=True, choices=SERVER_DICT.keys(), parser.add_argument('--server', required=True, choices=SERVER_DICT.keys(),
action='append', nargs='?', metavar='', action='append', nargs='?', metavar='',
help='Choose servers to connect to and compare.\n' help='Choose servers to connect to and compare.\n'
@ -272,7 +277,8 @@ if __name__ == "__main__":
main_dict = org_diff(combined_lst, opts.media_type, main_server.friendlyName) main_dict = org_diff(combined_lst, opts.media_type, main_server.friendlyName)
filename = 'diff_{}_{}_servers.json'.format(opts.server[0],'_'.join(servers)) filename = 'diff_{}_{}_servers.json'.format(
opts.server[0], '_'.join(servers))
with open(filename, 'w') as fp: with open(filename, 'w') as fp:
json.dump(main_dict, fp, indent=4, sort_keys=True) json.dump(main_dict, fp, indent=4, sort_keys=True)

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Use Tautulli to count how many plays per user occurred this week. Use Tautulli to count how many plays per user occurred this week.
Notify via Tautulli Notification Notify via Tautulli Notification
@ -10,7 +13,7 @@ import time
TODAY = int(time.time()) TODAY = int(time.time())
LASTWEEK = int(TODAY - 7 * 24 * 60 * 60) LASTWEEK = int(TODAY - 7 * 24 * 60 * 60)
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
SUBJECT_TEXT = "Tautulli Weekly Plays Per User" SUBJECT_TEXT = "Tautulli Weekly Plays Per User"
@ -29,12 +32,13 @@ class UserHIS(object):
self.full_title = d['full_title'] self.full_title = d['full_title']
self.date = d['date'] self.date = d['date']
def get_history(): def get_history():
# Get the Tautulli history. Count matters!!! # Get the Tautulli history. Count matters!!!
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
'cmd': 'get_history', 'cmd': 'get_history',
'length': 100000} 'length': 100000}
try: try:
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload) r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
response = r.json() response = r.json()
@ -42,10 +46,11 @@ def get_history():
res_data = response['response']['data']['data'] res_data = response['response']['data']['data']
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1 and return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1 and
LASTWEEK < d['date'] < TODAY] LASTWEEK < d['date'] < TODAY]
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
def send_notification(BODY_TEXT): def send_notification(BODY_TEXT):
# Format notification text # Format notification text
try: try:
@ -73,13 +78,15 @@ def send_notification(BODY_TEXT):
sys.stderr.write("Tautulli API 'notify' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'notify' request failed: {0}.".format(e))
return None return None
def add_to_dictlist(d, key, val): def add_to_dictlist(d, key, val):
if key not in d: if key not in d:
d[key] = [val] d[key] = [val]
else: else:
d[key].append(val) d[key].append(val)
user_dict ={}
user_dict = {}
notify_lst = [] notify_lst = []
[add_to_dictlist(user_dict, h.user, h.media) for h in get_history()] [add_to_dictlist(user_dict, h.user, h.media) for h in get_history()]
@ -106,7 +113,9 @@ BODY_TEXT = """\
</p> </p>
</body> </body>
</html> </html>
""".format(notify_lst="\n".join(notify_lst).encode("utf-8"),end=time.ctime(float(TODAY)), """.format(
start=time.ctime(float(LASTWEEK))) notify_lst="\n".join(notify_lst).encode("utf-8"),
end=time.ctime(float(TODAY)),
start=time.ctime(float(LASTWEEK)))
send_notification(BODY_TEXT) send_notification(BODY_TEXT)

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
import time import time
import argparse import argparse
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
@ -36,6 +38,7 @@ VERIFY_SSL = False
timestr = time.strftime("%Y%m%d-%H%M%S") timestr = time.strftime("%Y%m%d-%H%M%S")
class Connection: class Connection:
def __init__(self, url=None, apikey=None, verify_ssl=False): def __init__(self, url=None, apikey=None, verify_ssl=False):
self.url = url self.url = url
@ -47,15 +50,15 @@ class Connection:
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)
# Ignore verifying the SSL certificate # Ignore verifying the SSL certificate
if verify_ssl is False: if verify_ssl is False:
self.session.verify = False self.session.verify = False
# Disable the warning that the request is insecure, we know that... # Disable the warning that the request is insecure, we know that...
import urllib3 import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class Library(object): class Library(object):
def __init__(self, data=None): def __init__(self, data=None):
d = data or {} d = data or {}
@ -66,38 +69,38 @@ class Library(object):
try: try:
self.parent_count = d['parent_count'] self.parent_count = d['parent_count']
self.child_count = d['child_count'] self.child_count = d['child_count']
except Exception as e: except Exception:
# print(e) # print(e)
pass pass
class Tautulli: class Tautulli:
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection
def _call_api(self, cmd, payload, method='GET'): def _call_api(self, cmd, payload, method='GET'):
payload['cmd'] = cmd payload['cmd'] = cmd
payload['apikey'] = self.connection.apikey payload['apikey'] = self.connection.apikey
try: try:
response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload) response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload)
except RequestException as e: except RequestException as e:
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e)) print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
return return
try: try:
response_json = response.json() response_json = response.json()
except ValueError: except ValueError:
print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd)) print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd))
return return
if response_json['response']['result'] == 'success': if response_json['response']['result'] == 'success':
return response_json['response']['data'] return response_json['response']['data']
else: else:
error_msg = response_json['response']['message'] error_msg = response_json['response']['message']
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg)) print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
return return
def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None): def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None):
"""Call Tautulli's get_history api endpoint""" """Call Tautulli's get_history api endpoint"""
payload = {"order_column": "full_title", payload = {"order_column": "full_title",
@ -112,14 +115,14 @@ class Tautulli:
payload["start"] = start payload["start"] = start
if length: if length:
payload["lengh"] = length payload["lengh"] = length
history = self._call_api('get_history', payload) history = self._call_api('get_history', payload)
return [d for d in history['data'] if d['watched_status'] == 1] return [d for d in history['data'] if d['watched_status'] == 1]
def get_libraries(self): def get_libraries(self):
"""Call Tautulli's get_libraries api endpoint""" """Call Tautulli's get_libraries api endpoint"""
payload = {} payload = {}
return self._call_api('get_libraries', payload) return self._call_api('get_libraries', payload)
@ -131,7 +134,7 @@ class Plex:
if token and url: if token and url:
session = Connection().session session = Connection().session
self.server = PlexServer(baseurl=url, token=token, session=session) self.server = PlexServer(baseurl=url, token=token, session=session)
def all_users(self): def all_users(self):
"""All users """All users
Returns Returns
@ -141,9 +144,9 @@ class Plex:
users = {self.account.title: self.account} users = {self.account.title: self.account}
for user in self.account.users(): for user in self.account.users():
users[user.title] = user users[user.title] = user
return users return users
def all_sections(self): def all_sections(self):
"""All sections from server """All sections from server
Returns Returns
@ -152,7 +155,7 @@ class Plex:
{section title: section object} {section title: section object}
""" """
sections = {section.title: section for section in self.server.library.sections()} sections = {section.title: section for section in self.server.library.sections()}
return sections return sections
def all_sections_totals(self, library=None): def all_sections_totals(self, library=None):
@ -174,17 +177,17 @@ class Plex:
section_total = len(section.search(libtype='episode')) section_total = len(section.search(libtype='episode'))
else: else:
continue continue
if library: if library:
return section_total return section_total
section_totals[section.title] = section_total section_totals[section.title] = section_total
return section_totals return section_totals
def make_pie(user_dict, sections_dict, title, filename=None, headless=None): def make_pie(user_dict, sections_dict, title, filename=None, headless=None):
import matplotlib as mpl import matplotlib as mpl
mpl.rcParams['text.color'] = FONT_COLOR mpl.rcParams['text.color'] = FONT_COLOR
mpl.rcParams['axes.labelcolor'] = FONT_COLOR mpl.rcParams['axes.labelcolor'] = FONT_COLOR
@ -209,7 +212,7 @@ def make_pie(user_dict, sections_dict, title, filename=None, headless=None):
ax.pie(fracs, explode=EXPLODE, colors=COLORS, pctdistance=1.3, ax.pie(fracs, explode=EXPLODE, colors=COLORS, pctdistance=1.3,
autopct='%1.1f%%', shadow=True, startangle=300, radius=0.8, autopct='%1.1f%%', shadow=True, startangle=300, radius=0.8,
wedgeprops=dict(width=0.5, edgecolor=BACKGROUND_COLOR)) wedgeprops=dict(width=0.5, edgecolor=BACKGROUND_COLOR))
if user_position == 0: if user_position == 0:
ax.set_title("{}: {}".format(library, library_total), bbox=BBOX_PROPS, ax.set_title("{}: {}".format(library, library_total), bbox=BBOX_PROPS,
ha='center', va='bottom', size=12) ha='center', va='bottom', size=12)
@ -223,7 +226,7 @@ def make_pie(user_dict, sections_dict, title, filename=None, headless=None):
plt.suptitle(title, bbox=BBOX_PROPS, size=15) plt.suptitle(title, bbox=BBOX_PROPS, size=15)
plt.tight_layout() plt.tight_layout()
fig.subplots_adjust(top=0.88) fig.subplots_adjust(top=0.88)
if filename: if filename:
plt.savefig('{}_{}.png'.format(filename, timestr), facecolor=BACKGROUND_COLOR) plt.savefig('{}_{}.png'.format(filename, timestr), facecolor=BACKGROUND_COLOR)
print('Image saved as: {}_{}.png'.format(filename, timestr)) print('Image saved as: {}_{}.png'.format(filename, timestr))
@ -235,13 +238,13 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Show watched percentage of users by libraries.", parser = argparse.ArgumentParser(description="Show watched percentage of users by libraries.",
formatter_class=argparse.RawTextHelpFormatter) formatter_class=argparse.RawTextHelpFormatter)
servers = parser.add_mutually_exclusive_group() servers = parser.add_mutually_exclusive_group()
servers.add_argument('--plex', default=False, action='store_true', servers.add_argument('--plex', default=False, action='store_true',
help='Pull data from Plex') help='Pull data from Plex')
servers.add_argument('--tautulli', default=False, action='store_true', servers.add_argument('--tautulli', default=False, action='store_true',
help='Pull data from Tautulli') help='Pull data from Tautulli')
parser.add_argument('--libraries', nargs='*', metavar='library', parser.add_argument('--libraries', nargs='*', metavar='library',
help='Libraries to scan for watched content.') help='Libraries to scan for watched content.')
parser.add_argument('--users', nargs='*', metavar='users', parser.add_argument('--users', nargs='*', metavar='users',
@ -251,20 +254,19 @@ if __name__ == '__main__':
parser.add_argument('--filename', type=str, default='Users_Watched_{}'.format(timestr), metavar='', parser.add_argument('--filename', type=str, default='Users_Watched_{}'.format(timestr), metavar='',
help='Filename of pie chart. None will not save. \n(default: %(default)s)') help='Filename of pie chart. None will not save. \n(default: %(default)s)')
parser.add_argument('--headless', action='store_true', help='Run headless.') parser.add_argument('--headless', action='store_true', help='Run headless.')
opts = parser.parse_args() opts = parser.parse_args()
sections_totals_dict = {} sections_totals_dict = {}
sections_dict = {} sections_dict = {}
user_dict = {} user_dict = {}
title = "User's Watch Percentage by Library\nFrom: {}" title = "User's Watch Percentage by Library\nFrom: {}"
if opts.plex: if opts.plex:
admin_account = Plex(PLEX_TOKEN) admin_account = Plex(PLEX_TOKEN)
plex_server = Plex(PLEX_TOKEN, PLEX_URL) plex_server = Plex(PLEX_TOKEN, PLEX_URL)
title = title.format(plex_server.server.friendlyName) title = title.format(plex_server.server.friendlyName)
for library in opts.libraries: for library in opts.libraries:
section_total = plex_server.all_sections_totals(library) section_total = plex_server.all_sections_totals(library)
sections_totals_dict[library] = section_total sections_totals_dict[library] = section_total
@ -284,7 +286,7 @@ if __name__ == '__main__':
section_watched_total = len(section_watched_lst) section_watched_total = len(section_watched_lst)
percent_watched = 100 * (float(section_watched_total) / float(section_total)) percent_watched = 100 * (float(section_watched_total) / float(section_total))
print(" {} has watched {} items ({}%).".format(user, section_watched_total, int(percent_watched))) print(" {} has watched {} items ({}%).".format(user, section_watched_total, int(percent_watched)))
if user_dict.get(user): if user_dict.get(user):
user_dict[user].update({library: section_watched_total}) user_dict[user].update({library: section_watched_total})
else: else:
@ -295,7 +297,7 @@ if __name__ == '__main__':
user_dict[user].update({library: 0}) user_dict[user].update({library: 0})
else: else:
user_dict[user] = {library: 0} user_dict[user] = {library: 0}
elif opts.tautulli: elif opts.tautulli:
# Create a Tautulli instance # Create a Tautulli instance
tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'), tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'),
@ -307,7 +309,7 @@ if __name__ == '__main__':
for section in tautulli_sections: for section in tautulli_sections:
library = Library(section) library = Library(section)
sections_dict[library.title] = library sections_dict[library.title] = library
for library in opts.libraries: for library in opts.libraries:
section = sections_dict[library] section = sections_dict[library]
if section.type == "movie": if section.type == "movie":
@ -317,7 +319,7 @@ if __name__ == '__main__':
else: else:
print("Not doing that...") print("Not doing that...")
section_total = 0 section_total = 0
print("Section: {}, has {} items.".format(library, section_total)) print("Section: {}, has {} items.".format(library, section_total))
sections_totals_dict[library] = section_total sections_totals_dict[library] = section_total
for user in opts.users: for user in opts.users:
@ -337,7 +339,7 @@ if __name__ == '__main__':
elif not all([tt_watched]): elif not all([tt_watched]):
break break
start += count start += count
except Exception as e: except Exception as e:
print(user, e) print(user, e)
@ -351,4 +353,4 @@ if __name__ == '__main__':
user_dict[user] = {library: section_watched_total} user_dict[user] = {library: section_watched_total}
if opts.pie: if opts.pie:
make_pie(user_dict, sections_totals_dict, title, opts.filename, opts.headless) make_pie(user_dict, sections_totals_dict, title, opts.filename, opts.headless)

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Pull library and user statistics of last week. Pull library and user statistics of last week.
@ -13,7 +16,7 @@ import requests
import sys import sys
import time import time
import datetime import datetime
import json # import json
from operator import itemgetter from operator import itemgetter
import argparse import argparse
@ -72,6 +75,7 @@ BODY_TEXT = """\
# /EDIT THESE SETTINGS # # /EDIT THESE SETTINGS #
def get_history(section_id, check_date): def get_history(section_id, check_date):
# Get the Tautulli history. # Get the Tautulli history.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -156,7 +160,7 @@ def send_notification(body_text):
def sizeof_fmt(num, suffix='B'): def sizeof_fmt(num, suffix='B'):
# Function found https://stackoverflow.com/a/1094933 # Function found https://stackoverflow.com/a/1094933
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(num) < 1024.0: if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix) return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0 num /= 1024.0
@ -171,7 +175,7 @@ def date_split(to_split):
def add_to_dictval(d, key, val): def add_to_dictval(d, key, val):
#print(d, key, val) # print(d, key, val)
if key not in d: if key not in d:
d[key] = val d[key] = val
else: else:
@ -191,7 +195,7 @@ def get_server_stats(date_ranges):
user_stats_lst = [] user_stats_lst = []
user_stats_dict = {} user_stats_dict = {}
user_names_lst = [] user_names_lst = []
user_durations_lst =[] user_durations_lst = []
print('Checking library stats.') print('Checking library stats.')
for sections in get_libraries(): for sections in get_libraries():
@ -290,5 +294,6 @@ def main():
print('Sending message.') print('Sending message.')
send_notification(BODY_TEXT) send_notification(BODY_TEXT)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

11
setup.cfg Normal file
View File

@ -0,0 +1,11 @@
; Contains configuration for various linters
; E501: Disable line length limits (for now)
; W504: Require newlines after binary operators, use W503 for requiring the
; operators on the next line
[flake8]
ignore = E501,W504
[pylama]
ignore = E501,W504

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
"""
Use Tautulli to pull last IP address from user and add to List of IP addresses and networks that are allowed without auth in Plex. Use Tautulli to pull last IP address from user and add to List of IP addresses and networks that are allowed without auth in Plex.
optional arguments: optional arguments:
@ -13,14 +15,14 @@ optional arguments:
(default: None) (default: None)
List of IP addresses is cleared before adding new IPs List of IP addresses is cleared before adding new IPs
''' """
import requests import requests
import argparse import argparse
import sys import sys
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
PLEX_TOKEN = 'xxxx' PLEX_TOKEN = 'xxxx'
PLEX_URL = 'http://localhost:32400' PLEX_URL = 'http://localhost:32400'
TAUTULLI_APIKEY = 'xxxx' # Your Tautulli API key TAUTULLI_APIKEY = 'xxxx' # Your Tautulli API key
@ -78,7 +80,7 @@ if __name__ == '__main__':
parser.add_argument('-u', '--users', nargs='+', type=str, choices=user_lst, metavar='', parser.add_argument('-u', '--users', nargs='+', type=str, choices=user_lst, metavar='',
help='Space separated list of case sensitive names to process. Allowed names are: \n' help='Space separated list of case sensitive names to process. Allowed names are: \n'
'(choices: %(choices)s) \n(default: %(default)s)') '(choices: %(choices)s) \n(default: %(default)s)')
parser.add_argument('-c', '--clear', nargs='?',default=None, metavar='', parser.add_argument('-c', '--clear', nargs='?', default=None, metavar='',
help='Clear List of IP addresses and networks that are allowed without auth in Plex: \n' help='Clear List of IP addresses and networks that are allowed without auth in Plex: \n'
'(default: %(default)s)') '(default: %(default)s)')

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
From a list of TV shows, check if users in a list has watched shows episodes. From a list of TV shows, check if users in a list has watched shows episodes.
If all users in list have watched an episode of listed show, then delete episode. If all users in list have watched an episode of listed show, then delete episode.
@ -9,8 +12,7 @@ import requests
import sys import sys
import os import os
# ## EDIT THESE SETTINGS ##
## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
SHOW_LST = [123456, 123456, 123456, 123456] # Show rating keys. SHOW_LST = [123456, 123456, 123456, 123456] # Show rating keys.
@ -109,8 +111,9 @@ for user in USER_LST:
for meta_dict in meta_lst: for meta_dict in meta_lst:
if set(USER_LST) == set(meta_dict['watched_by']): if set(USER_LST) == set(meta_dict['watched_by']):
print("{} {} has been watched by {}".format(meta_dict['grandparent_title'].encode('UTF-8'), print("{} {} has been watched by {}".format(
meta_dict['title'].encode('UTF-8'), meta_dict['grandparent_title'].encode('UTF-8'),
" & ".join(USER_LST))) meta_dict['title'].encode('UTF-8'),
" & ".join(USER_LST)))
print("Removing {}".format(meta_dict['file'])) print("Removing {}".format(meta_dict['file']))
os.remove(meta_dict['file']) os.remove(meta_dict['file'])

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
""" # -*- coding: utf-8 -*-
Description: Enable or disable all users remote access to Tautulli
"""Enable or disable all users remote access to Tautulli.
Author: DirtyCajunRice Author: DirtyCajunRice
Requires: requests, python3.6+ Requires: requests, python3.6+
""" """

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
''' '''
Find location of Plex metadata. Find location of Plex metadata.
@ -18,7 +20,7 @@ import hashlib
import argparse import argparse
import requests import requests
## Edit ## # ## Edit ##
PLEX_URL = '' PLEX_URL = ''
PLEX_TOKEN = '' PLEX_TOKEN = ''
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL) PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
@ -28,7 +30,7 @@ PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
PLEX_LOCAL_TV_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\TV Shows') PLEX_LOCAL_TV_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\TV Shows')
PLEX_LOCAL_MOVIE_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\Movies') PLEX_LOCAL_MOVIE_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\Movies')
PLEX_LOCAL_ALBUM_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\Albums') PLEX_LOCAL_ALBUM_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\Albums')
## /Edit ## # ## /Edit ##
sess = requests.Session() sess = requests.Session()
# Ignore verifying the SSL certificate # Ignore verifying the SSL certificate
@ -44,6 +46,7 @@ if sess.verify is False:
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess) plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
def hash_to_path(hash_str, path, title, media_type, artist=None): def hash_to_path(hash_str, path, title, media_type, artist=None):
full_hash = hashlib.sha1(hash_str).hexdigest() full_hash = hashlib.sha1(hash_str).hexdigest()
hash_path = '{}\{}{}'.format(full_hash[0], full_hash[1::1], '.bundle') hash_path = '{}\{}{}'.format(full_hash[0], full_hash[1::1], '.bundle')
@ -54,12 +57,11 @@ def hash_to_path(hash_str, path, title, media_type, artist=None):
output = "{} titled: {}\nPath: {}".format(media_type.title(), title, full_path) output = "{} titled: {}\nPath: {}".format(media_type.title(), title, full_path)
print(output) print(output)
def get_plex_hash(search, mediatype=None): def get_plex_hash(search, mediatype=None):
for searched in plex.search(search, mediatype=mediatype): for searched in plex.search(search, mediatype=mediatype):
# Remove special characters from name
clean_title = re.sub('\W+',' ', searched.title)
if searched.type == 'show':
# Need to find guid. # Need to find guid.
if searched.type == 'show':
# Get tvdb_if from first episode # Get tvdb_if from first episode
db_id = searched.episodes()[0].guid db_id = searched.episodes()[0].guid
# Find str to pop # Find str to pop
@ -79,7 +81,7 @@ def get_plex_hash(search, mediatype=None):
hash_str = 'local://{}'.format(local_id) hash_str = 'local://{}'.format(local_id)
else: else:
hash_str = searched.tracks()[0].guid.replace('/1?lang=en', '?lang=en') hash_str = searched.tracks()[0].guid.replace('/1?lang=en', '?lang=en')
#print(searched.__dict__.items()) # print(searched.__dict__.items())
hash_to_path(hash_str, PLEX_LOCAL_ALBUM_PATH, searched.title, searched.type, searched.parentTitle) hash_to_path(hash_str, PLEX_LOCAL_ALBUM_PATH, searched.title, searched.type, searched.parentTitle)
elif searched.type == 'artist': elif searched.type == 'artist':
@ -89,6 +91,7 @@ def get_plex_hash(search, mediatype=None):
else: else:
pass pass
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Helping navigate Plex's locally stored data.") parser = argparse.ArgumentParser(description="Helping navigate Plex's locally stored data.")
parser.add_argument('-s', '--search', required=True, help='Search Plex for title.') parser.add_argument('-s', '--search', required=True, help='Search Plex for title.')

View File

@ -1,7 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Find what was added TFRAME ago and not watched using Tautulli. Find what was added TFRAME ago and not watched using Tautulli.
""" """
import requests import requests
@ -9,14 +10,14 @@ import sys
import time import time
import os import os
TFRAME = 1.577e+7 # ~ 6 months in seconds TFRAME = 1.577e+7 # ~ 6 months in seconds
TODAY = time.time() TODAY = time.time()
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
LIBRARY_NAMES = ['My TV Shows', 'My Movies'] # Name of libraries you want to check. LIBRARY_NAMES = ['My TV Shows', 'My Movies'] # Name of libraries you want to check.
class LIBINFO(object): class LIBINFO(object):
@ -81,7 +82,7 @@ def get_metadata(rating_key):
res_data = response['response']['data'] res_data = response['response']['data']
return METAINFO(data=res_data) return METAINFO(data=res_data)
except Exception as e: except Exception:
# sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e)) # sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e))
pass pass
@ -103,6 +104,7 @@ def get_library_media_info(section_id):
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e))
def get_libraries_table(): def get_libraries_table():
# Get the data on the Tautulli libraries table. # Get the data on the Tautulli libraries table.
payload = {'apikey': TAUTULLI_APIKEY, payload = {'apikey': TAUTULLI_APIKEY,
@ -117,7 +119,8 @@ def get_libraries_table():
except Exception as e: except Exception as e:
sys.stderr.write("Tautulli API 'get_libraries_table' request failed: {0}.".format(e)) sys.stderr.write("Tautulli API 'get_libraries_table' request failed: {0}.".format(e))
def delete_files(tmp_lst): def delete_files(tmp_lst):
del_file = raw_input('Delete all unwatched files? (yes/no)').lower() del_file = raw_input('Delete all unwatched files? (yes/no)').lower()
if del_file.startswith('y'): if del_file.startswith('y'):
@ -127,6 +130,7 @@ def delete_files(tmp_lst):
else: else:
print('Ok. doing nothing.') print('Ok. doing nothing.')
show_lst = [] show_lst = []
path_lst = [] path_lst = []

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" # -*- coding: utf-8 -*-
Description: Get a list of "Serial Transcoders"
"""Get a list of "Serial Transcoders".
Author: DirtyCajunRice Author: DirtyCajunRice
Requires: requests, plexapi, python3.6+ Requires: requests, plexapi, python3.6+
""" """

View File

@ -1,8 +1,9 @@
# -*- encoding: UTF-8 -*- #!/usr/bin/env python
# -*- coding: utf-8 -*-
''' """
https://gist.github.com/blacktwin/f435aa0ccd498b0840d2407d599bf31d https://gist.github.com/blacktwin/f435aa0ccd498b0840d2407d599bf31d
''' """
import os import os
import httplib2 import httplib2
@ -12,15 +13,11 @@ from oauth2client.file import Storage
from googleapiclient.discovery import build from googleapiclient.discovery import build
from oauth2client.client import OAuth2WebServerFlow from oauth2client.client import OAuth2WebServerFlow
import time, shutil, sys
# Copy your credentials from the console # Copy your credentials from the console
# https://console.developers.google.com # https://console.developers.google.com
CLIENT_ID = '' CLIENT_ID = ''
CLIENT_SECRET = '' CLIENT_SECRET = ''
OUT_PATH = '' # Output Path OUT_PATH = '' # Output Path
OAUTH_SCOPE = 'https://www.googleapis.com/auth/drive' OAUTH_SCOPE = 'https://www.googleapis.com/auth/drive'
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
@ -48,6 +45,7 @@ http = credentials.authorize(http)
drive_service = build('drive', 'v2', http=http) drive_service = build('drive', 'v2', http=http)
def list_files(service): def list_files(service):
page_token = None page_token = None
while True: while True:
@ -90,11 +88,11 @@ for item in list_files(drive_service):
if 'mimeType' in item and 'image/jpeg' in item['mimeType'] or 'video/mp4' in item['mimeType']: if 'mimeType' in item and 'image/jpeg' in item['mimeType'] or 'video/mp4' in item['mimeType']:
download_url = item['downloadUrl'] download_url = item['downloadUrl']
else: else:
print 'ERROR getting %s' % item.get('title') print('ERROR getting %s' % item.get('title'))
print item print(item)
print dir(item) print(dir(item))
if download_url: if download_url:
print "downloading %s" % item.get('title') print("downloading %s" % item.get('title'))
resp, content = drive_service._http.request(download_url) resp, content = drive_service._http.request(download_url)
if resp.status == 200: if resp.status == 200:
if os.path.isfile(outfile): if os.path.isfile(outfile):

View File

@ -1,13 +1,15 @@
#!/usr/bin/env python #!/usr/bin/env python
""" # -*- coding: utf-8 -*-
Description: Removes Shows from On Deck
"""Removes Shows from On Deck.
Author: Blacktwin Author: Blacktwin
Requires: requests, plexapi Requires: requests, plexapi
Example: Example:
python off_deck.py --action deck --user Steve python off_deck.py --action deck --user Steve
- Display what shows are on Steve's On Deck - Display what shows are on Steve's On Deck
python off_deck.py --action deck --shows "The Simpsons" Seinfeld python off_deck.py --action deck --shows "The Simpsons" Seinfeld
- The Simpsons and Seinfeld will be removed from On Deck - The Simpsons and Seinfeld will be removed from On Deck
@ -17,10 +19,10 @@ Requires: requests, plexapi
python off_deck.py --action deck --playlist "Favorite Shows!" python off_deck.py --action deck --playlist "Favorite Shows!"
- Any Show found in Favorite Shows playlist will be remove - Any Show found in Favorite Shows playlist will be remove
from On Deck from On Deck
python off_deck.py --action watch --user Steve python off_deck.py --action watch --user Steve
- Display what shows are on Steve's Continue Watching - Display what shows are on Steve's Continue Watching
python off_deck.py --action watch --shows "The Simpsons" Seinfeld python off_deck.py --action watch --shows "The Simpsons" Seinfeld
- The Simpsons and Seinfeld will be removed from Continue Watching - The Simpsons and Seinfeld will be removed from Continue Watching
@ -99,9 +101,9 @@ def get_con_watch(server, off_deck=None):
if item.grandparentTitle in off_deck: if item.grandparentTitle in off_deck:
print('Removing {}: S{:02}E{:02} {} from Continue Watching ' print('Removing {}: S{:02}E{:02} {} from Continue Watching '
'by marking watched.'.format( 'by marking watched.'.format(
item.grandparentTitle.encode('UTF-8'), item.grandparentTitle.encode('UTF-8'),
int(item.parentIndex), int(item.index), int(item.parentIndex), int(item.index),
item.title.encode('UTF-8'))) item.title.encode('UTF-8')))
item.markWatched() item.markWatched()
else: else:
if item.type == 'episode' and item.viewOffset > 0: if item.type == 'episode' and item.viewOffset > 0:
@ -115,7 +117,7 @@ def get_con_watch(server, off_deck=None):
item.grandparentTitle.encode('UTF-8'), item.grandparentTitle.encode('UTF-8'),
int(item.parentIndex), int(item.index), int(item.parentIndex), int(item.index),
item.title.encode('UTF-8'), offset)) item.title.encode('UTF-8'), offset))
def get_on_deck(server, off_deck=None): def get_on_deck(server, off_deck=None):
""" """
@ -142,12 +144,13 @@ def get_on_deck(server, off_deck=None):
watched_statuses['grandparent'] = grandparent watched_statuses['grandparent'] = grandparent
watched_statuses['episodes'] = [] watched_statuses['episodes'] = []
for episode in grandparent.episodes(): for episode in grandparent.episodes():
watched_statuses['episodes'].append({'object': episode, watched_statuses['episodes'].append({
'viewCount': episode.viewCount}) 'object': episode,
'viewCount': episode.viewCount})
else: else:
if item.type == 'episode': if item.type == 'episode':
on_deck.append(item) on_deck.append(item)
if on_deck: if on_deck:
watched_statuses['on_deck'] = on_deck watched_statuses['on_deck'] = on_deck
return watched_statuses return watched_statuses
@ -204,7 +207,7 @@ if __name__ == '__main__':
off_deck['grandparent'].markUnwatched() off_deck['grandparent'].markUnwatched()
else: else:
break break
print('Resetting watch counts...') print('Resetting watch counts...')
for item in ep_list: for item in ep_list:
print('Resetting view count for {}: S{:02}E{:02} {}'.format( print('Resetting view count for {}: S{:02}E{:02} {}'.format(
@ -214,7 +217,7 @@ if __name__ == '__main__':
# if viewCount was 0 then make 1 so as not to return to On Deck. # if viewCount was 0 then make 1 so as not to return to On Deck.
for _ in range(item['viewCount'] if item['viewCount'] != 0 else 1): for _ in range(item['viewCount'] if item['viewCount'] != 0 else 1):
item['object'].markWatched() item['object'].markWatched()
elif opts.action == 'watch': elif opts.action == 'watch':
print('Finding shows marked Continue Watching...') print('Finding shows marked Continue Watching...')
get_con_watch(plex_server, to_remove) get_con_watch(plex_server, to_remove)

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
"""
Invite new users to share Plex libraries. Invite new users to share Plex libraries.
optional arguments: optional arguments:
@ -33,7 +35,7 @@ Usage:
plex_api_invite.py --libraries Movies --user USER --movieRatings G, PG-13 plex_api_invite.py --libraries Movies --user USER --movieRatings G, PG-13
- Share Movie library with USER but restrict them to only G and PG-13 titles. - Share Movie library with USER but restrict them to only G and PG-13 titles.
''' """
from plexapi.server import PlexServer, CONFIG from plexapi.server import PlexServer, CONFIG
import argparse import argparse
@ -71,11 +73,11 @@ def invite(user, sections, allowSync, camera, channels, filterMovies, filterTele
allowCameraUpload=camera, allowChannels=channels, filterMovies=filterMovies, allowCameraUpload=camera, allowChannels=channels, filterMovies=filterMovies,
filterTelevision=filterTelevision, filterMusic=filterMusic) filterTelevision=filterTelevision, filterMusic=filterMusic)
print('Invited {user} to share libraries: \n{sections}'.format(sections=sections, user=user)) print('Invited {user} to share libraries: \n{sections}'.format(sections=sections, user=user))
if allowSync == True: if allowSync is True:
print('Sync: Enabled') print('Sync: Enabled')
if camera == True: if camera is True:
print('Camera Upload: Enabled') print('Camera Upload: Enabled')
if channels == True: if channels is True:
print('Plugins: Enabled') print('Plugins: Enabled')
if filterMovies: if filterMovies:
print('Movie Filters: {}'.format(filterMovies)) print('Movie Filters: {}'.format(filterMovies))
@ -161,4 +163,4 @@ if __name__ == "__main__":
for user in opts.user: for user in opts.user:
invite(user, libraries, sync, camera, channels, invite(user, libraries, sync, camera, channels,
filterMovies, filterTelevision, filterMusic) filterMovies, filterTelevision, filterMusic)

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
"""
Set as cron or task for times of allowing and not allowing user access to server. Set as cron or task for times of allowing and not allowing user access to server.
Unsharing will kill any current stream from user before unsharing. Unsharing will kill any current stream from user before unsharing.
@ -33,7 +35,7 @@ Usage:
- Unshared all libraries with USER. - Unshared all libraries with USER.
- USER is still exists as a Friend or Home User - USER is still exists as a Friend or Home User
''' """
import argparse import argparse

View File

@ -1,6 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
""" # -*- coding: utf-8 -*-
Description: Pull Movie and TV Show poster images from Plex. Save to Movie and TV Show directories in scripts working directory.
"""Pull Movie and TV Show poster images from Plex.
Saves the poster images to Movie and TV Show directories in scripts working
directory.
Author: Blacktwin Author: Blacktwin
Requires: plexapi Requires: plexapi
@ -15,7 +20,7 @@ import re
import os import os
import urllib import urllib
library_name = ['Movies','TV Shows'] # You library names library_name = ['Movies', 'TV Shows'] # Your library names
PLEX_URL = '' PLEX_URL = ''
PLEX_TOKEN = '' PLEX_TOKEN = ''
@ -50,7 +55,7 @@ if not os.path.isdir(show_path):
for library in library_name: for library in library_name:
for child in plex.library.section(library).all(): for child in plex.library.section(library).all():
# Clean names of special characters # Clean names of special characters
name = re.sub('\W+',' ', child.title) name = re.sub('\W+', ' ', child.title)
# Add (year) to name # Add (year) to name
name = '{} ({})'.format(name, child.year) name = '{} ({})'.format(name, child.year)
# Pull URL for poster # Pull URL for poster

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
Share or unshare libraries.
"""Share or unshare libraries.
optional arguments: optional arguments:
-h, --help Show this help message and exit -h, --help Show this help message and exit
@ -65,7 +66,7 @@ Usage:
plex_api_share.py --backup plex_api_share.py --backup
- Backup all user shares to a json file - Backup all user shares to a json file
plex_api_share.py --backup --user USER plex_api_share.py --backup --user USER
- Backup USER shares to a json file - Backup USER shares to a json file
@ -96,7 +97,7 @@ Usage:
plex_api_share.py --share -u USER --allLibraries --libraries Movies plex_api_share.py --share -u USER --allLibraries --libraries Movies
- Shared [all libraries but Movies] with USER. - Shared [all libraries but Movies] with USER.
''' """
from plexapi.server import PlexServer, CONFIG from plexapi.server import PlexServer, CONFIG
import time import time
@ -137,13 +138,14 @@ movies_keys = [x.key for x in plex.library.sections() if x.type == 'movie']
show_keys = [x.key for x in plex.library.sections() if x.type == 'show'] show_keys = [x.key for x in plex.library.sections() if x.type == 'show']
json_check = sorted([f for f in os.listdir('.') if os.path.isfile(f) and json_check = sorted([f for f in os.listdir('.') if os.path.isfile(f) and
f.endswith(".json") and f.startswith(plex.friendlyName)], f.endswith(".json") and
f.startswith(plex.friendlyName)],
key=os.path.getmtime) key=os.path.getmtime)
my_server_names = [] my_server_names = []
# Find all owners server names. For owners with multiple servers. # Find all owners server names. For owners with multiple servers.
for res in plex.myPlexAccount().resources(): for res in plex.myPlexAccount().resources():
if res.provides == 'server' and res.owned == True: if res.provides == 'server' and res.owned is True:
my_server_names.append(res.name) my_server_names.append(res.name)
@ -151,7 +153,7 @@ def get_ratings_lst(section_id):
headers = {'Accept': 'application/json'} headers = {'Accept': 'application/json'}
params = {'X-Plex-Token': PLEX_TOKEN} params = {'X-Plex-Token': PLEX_TOKEN}
content = sess.get("{}/library/sections/{}/contentRating".format(PLEX_URL, section_id), content = sess.get("{}/library/sections/{}/contentRating".format(PLEX_URL, section_id),
headers=headers, params=params) headers=headers, params=params)
ratings_keys = content.json()['MediaContainer']['Directory'] ratings_keys = content.json()['MediaContainer']['Directory']
ratings_lst = [x['title'] for x in ratings_keys] ratings_lst = [x['title'] for x in ratings_keys]
@ -166,7 +168,7 @@ def filter_clean(filter_type):
labels = v.replace('%20', ' ') labels = v.replace('%20', ' ')
labels = labels.split('%2C') labels = labels.split('%2C')
clean[k] = labels clean[k] = labels
except Exception as e: except Exception:
pass pass
return clean return clean
@ -193,7 +195,7 @@ def find_shares(user):
if server.name == plex.friendlyName: if server.name == plex.friendlyName:
sections = [] sections = []
for section in server.sections(): for section in server.sections():
if section.shared == True: if section.shared is True:
sections.append(section.title) sections.append(section.title)
user_backup['sections'] = sections user_backup['sections'] = sections
@ -219,17 +221,17 @@ def share(user, sections, allowSync, camera, channels, filterMovies, filterTelev
filterTelevision=filterTelevision, filterMusic=filterMusic) filterTelevision=filterTelevision, filterMusic=filterMusic)
if sections: if sections:
print('{user}\'s updated shared libraries: \n{sections}'.format(sections=sections, user=user)) print('{user}\'s updated shared libraries: \n{sections}'.format(sections=sections, user=user))
if allowSync == True: if allowSync is True:
print('Sync: Enabled') print('Sync: Enabled')
if allowSync == False: if allowSync is False:
print('Sync: Disabled') print('Sync: Disabled')
if camera == True: if camera is True:
print('Camera Upload: Enabled') print('Camera Upload: Enabled')
if camera == False: if camera is False:
print('Camera Upload: Disabled') print('Camera Upload: Disabled')
if channels == True: if channels is True:
print('Plugins: Enabled') print('Plugins: Enabled')
if channels == False: if channels is False:
print('Plugins: Disabled') print('Plugins: Disabled')
if filterMovies: if filterMovies:
print('Movie Filters: {}'.format(filterMovies)) print('Movie Filters: {}'.format(filterMovies))
@ -241,7 +243,7 @@ def share(user, sections, allowSync, camera, channels, filterMovies, filterTelev
print('Show Filters:') print('Show Filters:')
if filterMusic: if filterMusic:
print('Music Filters: {}'.format(filterMusic)) print('Music Filters: {}'.format(filterMusic))
if filterMusic == {} and filterMusic != None: if filterMusic == {} and filterMusic is not None:
print('Music Filters:') print('Music Filters:')
@ -286,7 +288,7 @@ if __name__ == "__main__":
help='Show all shares by library.') help='Show all shares by library.')
# For Plex Pass members # For Plex Pass members
if plex.myPlexSubscription == True: if plex.myPlexSubscription is True:
movie_ratings = [] movie_ratings = []
show_ratings = [] show_ratings = []
for movie in movies_keys: for movie in movies_keys:
@ -371,7 +373,7 @@ if __name__ == "__main__":
for k, v in user_lst.items(): for k, v in user_lst.items():
if v == user: if v == user:
del user_lst[k] del user_lst[k]
users = user_lst.keys() users = user_lst.keys()
# Defining libraries # Defining libraries
@ -384,7 +386,7 @@ if __name__ == "__main__":
for library in opts.libraries: for library in opts.libraries:
sections_lst.remove(library) sections_lst.remove(library)
libraries = sections_lst libraries = sections_lst
if opts.libraryShares: if opts.libraryShares:
users = user_lst.keys() users = user_lst.keys()
user_sections = {} user_sections = {}
@ -396,11 +398,10 @@ if __name__ == "__main__":
for user, sections in user_sections.items(): for user, sections in user_sections.items():
for section in sections: for section in sections:
section_users.setdefault(section, []).append(user) section_users.setdefault(section, []).append(user)
for section, users in section_users.items(): for section, users in section_users.items():
print("{} is shared to the following users:\n {}\n".format(section, ", ".join(users))) print("{} is shared to the following users:\n {}\n".format(section, ", ".join(users)))
# Share, Unshare, Kill, Add, or Remove # Share, Unshare, Kill, Add, or Remove
for user in users: for user in users:
user_shares = find_shares(user) user_shares = find_shares(user)
@ -485,4 +486,4 @@ if __name__ == "__main__":
print('Restoring user {}\'s shares and settings...'.format(user['title'])) print('Restoring user {}\'s shares and settings...'.format(user['title']))
share(user['title'], user['sections'], user['allowSync'], user['camera'], share(user['title'], user['sections'], user['allowSync'], user['camera'],
user['channels'], user['filterMovies'], user['filterTelevision'], user['channels'], user['filterMovies'], user['filterTelevision'],
user['filterMusic']) user['filterMusic'])

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
Change show deletion settings by library. Change show deletion settings by library.
@ -33,7 +35,7 @@ PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
# Allowed days/episodes to keep or delete # Allowed days/episodes to keep or delete
WATCHED_LST = [0, 1, 7] WATCHED_LST = [0, 1, 7]
UNWATCHED_LST = [0, 5, 3, 1, -3, -7,-30] UNWATCHED_LST = [0, 5, 3, 1, -3, -7, -30]
sess = requests.Session() sess = requests.Session()
# Ignore verifying the SSL certificate # Ignore verifying the SSL certificate
@ -57,10 +59,10 @@ def set_show(rating_key, action, number):
path = '{}/prefs'.format(rating_key) path = '{}/prefs'.format(rating_key)
try: try:
params = {'X-Plex-Token': PLEX_TOKEN, params = {'X-Plex-Token': PLEX_TOKEN,
action: number action: number
} }
r = requests.put(PLEX_URL + path, params=params, verify=False) r = requests.put(PLEX_URL + path, params=params, verify=False)
print(r.url) print(r.url)
except Exception as e: except Exception as e:
print('Error: {}'.format(e)) print('Error: {}'.format(e))

View File

@ -1,27 +1,30 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
"""
Pull poster images from Imgur and places them inside Shows root folder. Pull poster images from Imgur and places them inside Shows root folder.
/path/to/show/Show.jpg /path/to/show/Show.jpg
Skips download if showname.jpg exists or if show does not exist. Skips download if showname.jpg exists or if show does not exist.
''' """
import requests import requests
import urllib import urllib
import os import os
## Edit ## # ## Edit ##
# Imgur info # Imgur info
CLIENT_ID = 'xxxxx' # Tautulli Settings > Notifications > Imgur Client ID CLIENT_ID = 'xxxxx' # Tautulli Settings > Notifications > Imgur Client ID
ALBUM_ID = '7JeSw' # http://imgur.com/a/7JeSw <--- 7JeSw is the ablum_id ALBUM_ID = '7JeSw' # http://imgur.com/a/7JeSw <--- 7JeSw is the ablum_id
# Local info # Local info
SHOW_PATH = 'D:\\Shows\\' SHOW_PATH = 'D:\\Shows\\'
## /Edit ## # ## /Edit ##
class IMGURINFO(object): class IMGURINFO(object):
def __init__(self, data=None): def __init__(self, data=None):

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
Build playlist from popular tracks.
"""Build playlist from popular tracks.
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -14,7 +15,7 @@ optional arguments:
* LIBRARY_EXCLUDE are excluded from libraries choice. * LIBRARY_EXCLUDE are excluded from libraries choice.
''' """
import requests import requests
@ -59,8 +60,8 @@ def fetch(path):
header = {'Accept': 'application/json'} header = {'Accept': 'application/json'}
params = {'X-Plex-Token': PLEX_TOKEN, params = {'X-Plex-Token': PLEX_TOKEN,
'includePopularLeaves': '1' 'includePopularLeaves': '1'
} }
r = requests.get(url + path, headers=header, params=params, verify=False) r = requests.get(url + path, headers=header, params=params, verify=False)
return r.json()['MediaContainer']['Metadata'][0]['PopularLeaves']['Metadata'] return r.json()['MediaContainer']['Metadata'][0]['PopularLeaves']['Metadata']
@ -97,7 +98,7 @@ if __name__ == "__main__":
parser.add_argument('--tracks', nargs='?', default=False, type=int, metavar='', parser.add_argument('--tracks', nargs='?', default=False, type=int, metavar='',
help='Specify the track length you would like the playlist.') help='Specify the track length you would like the playlist.')
parser.add_argument('--random',nargs='?', default=False, type=int, metavar='', parser.add_argument('--random', nargs='?', default=False, type=int, metavar='',
help='Randomly select N artists.') help='Randomly select N artists.')
opts = parser.parse_args() opts = parser.parse_args()
@ -128,7 +129,7 @@ if __name__ == "__main__":
if opts.tracks and opts.random: if opts.tracks and opts.random:
playlist = random.sample((playlist), opts.tracks) playlist = random.sample((playlist), opts.tracks)
elif opts.tracks and not opts.random: elif opts.tracks and not opts.random:
playlist = playlist[:opts.tracks] playlist = playlist[:opts.tracks]

View File

@ -1,9 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
Download theme songs from Plex TV Shows. Theme songs are mp3 and named by shows as displayed by Plex.
"""Download theme songs from Plex TV Shows.
Theme songs are mp3 and named by shows as displayed by Plex.
Songs are saved in a 'Theme Songs' directory located in script's path. Songs are saved in a 'Theme Songs' directory located in script's path.
''' """
from plexapi.server import PlexServer, CONFIG from plexapi.server import PlexServer, CONFIG
@ -13,14 +16,14 @@ import re
import urllib import urllib
import requests import requests
## Edit ## # ## Edit ##
PLEX_URL = '' PLEX_URL = ''
PLEX_TOKEN = '' PLEX_TOKEN = ''
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL) PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN) PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
TV_LIBRARY = 'TV Shows' # Name of your TV Show library TV_LIBRARY = 'TV Shows' # Name of your TV Show library
## /Edit ## # ## /Edit ##
sess = requests.Session() sess = requests.Session()
# Ignore verifying the SSL certificate # Ignore verifying the SSL certificate
@ -47,7 +50,7 @@ if not os.path.isdir(out_path):
# Get episodes from TV Shows # Get episodes from TV Shows
for show in plex.library.section(TV_LIBRARY).all(): for show in plex.library.section(TV_LIBRARY).all():
# Remove special characters from name # Remove special characters from name
filename = '{}.mp3'.format(re.sub('\W+',' ', show.title)) filename = '{}.mp3'.format(re.sub('\W+', ' ', show.title))
# Set output path # Set output path
theme_path = os.path.join(out_path, filename) theme_path = os.path.join(out_path, filename)
# Get tvdb_if from first episode, no need to go through all episodes # Get tvdb_if from first episode, no need to go through all episodes

View File

@ -1,5 +1,7 @@
""" #!/usr/bin/env python
Delete all playlists from Plex using PlexAPI # -*- coding: utf-8 -*-
"""Delete all playlists from Plex.
""" """

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" # -*- coding: utf-8 -*-
Description: Purge Tautulli users that no longer exist as a friend in Plex
"""Purge Tautulli users that no longer exist as a friend in Plex.
Author: DirtyCajunRice Author: DirtyCajunRice
Requires: requests, plexapi, python3.6+ Requires: requests, plexapi, python3.6+
""" """

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
Refresh the next episode of show once current episode is watched.
"""Refresh the next episode of show once current episode is watched.
Check Tautulli's Watched Percent in Tautulli > Settings > General Check Tautulli's Watched Percent in Tautulli > Settings > General
1. Tautulli > Settings > Notification Agents > Scripts > Bell icon: 1. Tautulli > Settings > Notification Agents > Scripts > Bell icon:
@ -12,7 +14,7 @@ Check Tautulli's Watched Percent in Tautulli > Settings > General
3. Tautulli > Settings > Notifications > Script > Script Arguments: 3. Tautulli > Settings > Notifications > Script > Script Arguments:
{show_name} {episode_num00} {season_num00} {show_name} {episode_num00} {season_num00}
''' """
import requests import requests
import sys import sys
@ -42,7 +44,7 @@ plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
show_name = sys.argv[1] show_name = sys.argv[1]
next_ep_num = int(sys.argv[2]) next_ep_num = int(sys.argv[2])
season_num = int(sys.argv[3]) season_num = int(sys.argv[3])
TV_LIBRARY = 'My TV Shows' # Name of your TV Shows library TV_LIBRARY = 'My TV Shows' # Name of your TV Shows library
current_season = season_num - 1 current_season = season_num - 1

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
""" # -*- coding: utf-8 -*-
Unshare or Remove users who have been inactive for X days. Prints out last seen for all users.
"""Unshare or Remove users who have been inactive for X days. Prints out last seen for all users.
Just run. Just run.
@ -114,6 +115,3 @@ for user in TAUTULLI_USERS:
else: else:
print('{}, and has reached their inactivity limit. Unsharing.'.format(OUTPUT)) print('{}, and has reached their inactivity limit. Unsharing.'.format(OUTPUT))
ACCOUNT.updateFriend(PLEX_USERS[UID], SERVER, removeSections=True) ACCOUNT.updateFriend(PLEX_USERS[UID], SERVER, removeSections=True)

View File

@ -1,6 +1,7 @@
""" #!/usr/bin/env python
Find Movies that have been watched by a list of users. # -*- coding: utf-8 -*-
If all users have watched movie than delete.
"""Find and delete Movies that have been watched by a list of users.
Deletion is prompted Deletion is prompted
""" """
@ -11,11 +12,12 @@ import os
import shutil import shutil
## EDIT THESE SETTINGS ## # ## EDIT THESE SETTINGS ##
TAUTULLI_APIKEY = 'xxxxxxxx' # Your Tautulli API key TAUTULLI_APIKEY = 'xxxxxxxx' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
LIBRARY_NAMES = ['My Movies'] # Whatever your movie libraries are called. LIBRARY_NAMES = ['My Movies'] # Whatever your movie libraries are called.
USER_LST = ['Joe', 'Alex'] # Name of users USER_LST = ['Joe', 'Alex'] # Name of users
class UserHIS(object): class UserHIS(object):
def __init__(self, data=None): def __init__(self, data=None):
@ -81,6 +83,7 @@ def delete_files(tmp_lst):
else: else:
print('Ok. doing nothing.') print('Ok. doing nothing.')
movie_dict = {} movie_dict = {}
movie_lst = [] movie_lst = []
delete_lst = [] delete_lst = []
@ -119,7 +122,7 @@ for user in USER_LST:
for movie_dict in movie_lst: for movie_dict in movie_lst:
if set(USER_LST) == set(movie_dict['watched_by']): if set(USER_LST) == set(movie_dict['watched_by']):
print(u"{} has been watched by {}".format(movie_dict['title']," & ".join(USER_LST))) print(u"{} has been watched by {}".format(movie_dict['title'], " & ".join(USER_LST)))
delete_lst.append(movie_dict['file']) delete_lst.append(movie_dict['file'])
delete_files(delete_lst) delete_files(delete_lst)

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
""" # -*- coding: utf-8 -*-
Share functions from https://gist.github.com/JonnyWong16/f8139216e2748cb367558070c1448636
"""Share functions from https://gist.github.com/JonnyWong16/f8139216e2748cb367558070c1448636
Once user stream count hits LIMIT they are unshared from libraries expect for banned library. Once user stream count hits LIMIT they are unshared from libraries expect for banned library.
Once user stream count is below LIMIT and banned library video is watched their shares are restored. Once user stream count is below LIMIT and banned library video is watched their shares are restored.
@ -30,7 +31,6 @@ Tautulli will continue displaying that user is watching after unshare is execute
Tautulli will update after ~5 minutes and no longer display user's stream in ACTIVITY. Tautulli will update after ~5 minutes and no longer display user's stream in ACTIVITY.
Tautulli will think that user has stopped. Tautulli will think that user has stopped.
Create new library with one video. Create new library with one video.
Name library and video whatever you want. Name library and video whatever you want.
@ -59,7 +59,7 @@ import email.utils
import smtplib import smtplib
## EDIT THESE SETTINGS ### # ## EDIT THESE SETTINGS ###
TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
@ -74,8 +74,8 @@ SERVER_ID = "XXXXX" # Example: https://i.imgur.com/EjaMTUk.png
# UserID2: [LibraryID1, LibraryID2]} # UserID2: [LibraryID1, LibraryID2]}
USER_LIBRARIES = {123456: [123456, 123456, 123456, 123456, 123456, 123456]} USER_LIBRARIES = {123456: [123456, 123456, 123456, 123456, 123456, 123456]}
BAN_LIBRARY = {123456: [123456]} # {UserID1: [LibraryID1], UserID2: [LibraryID1]} BAN_LIBRARY = {123456: [123456]} # {UserID1: [LibraryID1], UserID2: [LibraryID1]}
BAN_RATING = 123456 # Banned rating_key to check if it's been watched. BAN_RATING = 123456 # Banned rating_key to check if it's been watched.
LIMIT = 3 LIMIT = 3
VIOLATION_LIMIT = 3 VIOLATION_LIMIT = 3
@ -99,15 +99,15 @@ BODY_TEXT = """\
""" """
# Email settings # Email settings
name = '' # Your name name = '' # Your name
sender = '' # From email address sender = '' # From email address
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com) email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
email_port = 587 # Email port (Gmail: 587) email_port = 587 # Email port (Gmail: 587)
email_username = '' # Your email username email_username = '' # Your email username
email_password = '' # Your email password email_password = '' # Your email password
## DO NOT EDIT BELOW ## # ## DO NOT EDIT BELOW ##
class Activity(object): class Activity(object):
def __init__(self, data=None): def __init__(self, data=None):
@ -216,7 +216,7 @@ def unshare(user_id):
return return
elif r.status_code == 400: elif r.status_code == 400:
print r.content print(r.content)
return return
elif r.status_code == 200: elif r.status_code == 200:
@ -256,6 +256,7 @@ def get_activity():
except Exception as e: 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))
def send_notification(to=None, friendly=None, val_cnt=None, val_tot=None, mess=None): def send_notification(to=None, friendly=None, val_cnt=None, val_tot=None, mess=None):
# Format notification text # Format notification text
try: try:
@ -295,9 +296,9 @@ if __name__ == "__main__":
try: try:
if act_lst.count(user) >= LIMIT: if act_lst.count(user) >= LIMIT:
# Trigger for first and next violation # Trigger for first and next violation
unshare(user) # Remove libraries unshare(user) # Remove libraries
share(user, BAN) # Share banned library share(user, BAN) # Share banned library
sys.stdout.write("Shared BAN_LIBRARY with user {0}".format(i)) sys.stdout.write("Shared BAN_LIBRARY with user {0}".format(user))
if type(history) is int: if type(history) is int:
# Next violation, history of banned video. # Next violation, history of banned video.
send_notification(mail_add, friendly, history, VIOLATION_LIMIT, FIRST_WARN) send_notification(mail_add, friendly, history, VIOLATION_LIMIT, FIRST_WARN)
@ -308,10 +309,10 @@ if __name__ == "__main__":
elif type(history) is int: elif type(history) is int:
# Trigger to share # Trigger to share
if share(user, UNBAN) == 400: if share(user, UNBAN) == 400:
exit() # User has history of watching banned video but libraries are already restored. exit() # User has history of watching banned video but libraries are already restored.
else: else:
unshare(user) # Remove banned library unshare(user) # Remove banned library
share(user, UNBAN) # Restore libraries share(user, UNBAN) # Restore libraries
elif history == 'ban': elif history == 'ban':
# Trigger for ban # Trigger for ban
unshare(user) unshare(user)

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
""" # -*- coding: utf-8 -*-
Description: Sync the watch status from one Plex or Tautulli user to other users across any owned server.
"""Sync the watch status from one Plex or Tautulli user to other users across any owned server.
Author: Blacktwin Author: Blacktwin
Requires: requests, plexapi, argparse Requires: requests, plexapi, argparse
@ -49,7 +51,7 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
- Synced watch statuses from Tautulli {title from library} to {USER2 or USER3}'s account on selected servers. - Synced watch statuses from Tautulli {title from library} to {USER2 or USER3}'s account on selected servers.
sync_watch_status.py --userFrom USER1=Tautulli --userTo USER2=Server1 USER3=Server2 --ratingKey 1234 sync_watch_status.py --userFrom USER1=Tautulli --userTo USER2=Server1 USER3=Server2 --ratingKey 1234
- Synced watch status of rating key 1234 from USER1's Tautulli history to {USER2 or USER3}'s account - Synced watch statuse of rating key 1234 from USER1's Tautulli history to {USER2 or USER3}'s account
on selected servers. on selected servers.
**Rating key must be a movie or episode. Shows and Seasons not support.... yet. **Rating key must be a movie or episode. Shows and Seasons not support.... yet.
""" """
@ -87,7 +89,7 @@ class Connection:
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)
# Ignore verifying the SSL certificate # Ignore verifying the SSL certificate
if verify_ssl is False: if verify_ssl is False:
self.session.verify = False self.session.verify = False
@ -115,7 +117,7 @@ class Metadata(object):
self.title = ep_name.lstrip() self.title = ep_name.lstrip()
else: else:
self.title = d['full_title'] self.title = d['full_title']
# For History # For History
try: try:
if d['watched_status']: if d['watched_status']:
@ -133,32 +135,32 @@ class Metadata(object):
class Tautulli: class Tautulli:
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection
def _call_api(self, cmd, payload, method='GET'): def _call_api(self, cmd, payload, method='GET'):
payload['cmd'] = cmd payload['cmd'] = cmd
payload['apikey'] = self.connection.apikey payload['apikey'] = self.connection.apikey
try: try:
response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload) response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload)
except RequestException as e: except RequestException as e:
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e)) print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
return return
try: try:
response_json = response.json() response_json = response.json()
except ValueError: except ValueError:
print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd)) print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd))
return return
if response_json['response']['result'] == 'success': if response_json['response']['result'] == 'success':
return response_json['response']['data'] return response_json['response']['data']
else: else:
error_msg = response_json['response']['message'] error_msg = response_json['response']['message']
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg)) print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
return return
def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None): def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None):
"""Call Tautulli's get_history api endpoint""" """Call Tautulli's get_history api endpoint."""
payload = {"order_column": "full_title", payload = {"order_column": "full_title",
"order_dir": "asc"} "order_dir": "asc"}
if user: if user:
@ -171,20 +173,18 @@ class Tautulli:
payload["start"] = start payload["start"] = start
if length: if length:
payload["lengh"] = length payload["lengh"] = length
history = self._call_api('get_history', payload) history = self._call_api('get_history', payload)
return [d for d in history['data'] if d['watched_status'] == 1] return [d for d in history['data'] if d['watched_status'] == 1]
def get_metadata(self, rating_key): def get_metadata(self, rating_key):
"""Call Tautulli's get_metadata api endpoint""" """Call Tautulli's get_metadata api endpoint."""
payload = {"rating_key": rating_key} payload = {"rating_key": rating_key}
return self._call_api('get_metadata', payload) return self._call_api('get_metadata', payload)
def get_libraries(self): def get_libraries(self):
"""Call Tautulli's get_libraries api endpoint""" """Call Tautulli's get_libraries api endpoint."""
payload = {} payload = {}
return self._call_api('get_libraries', payload) return self._call_api('get_libraries', payload)
@ -196,37 +196,43 @@ class Plex:
if token and url: if token and url:
session = Connection().session session = Connection().session
self.server = PlexServer(baseurl=url, token=token, session=session) self.server = PlexServer(baseurl=url, token=token, session=session)
def admin_servers(self): def admin_servers(self):
"""All owned servers """Get all owned servers.
Returns Returns
------- -------
data: dict data: dict
""" """
resources = {} resources = {}
for resource in self.account.resources(): for resource in self.account.resources():
if 'server' in [resource.provides] and resource.owned == True: if 'server' in [resource.provides] and resource.owned is True:
resources[resource.name] = resource resources[resource.name] = resource
return resources return resources
def all_users(self): def all_users(self):
"""All users """Get all users.
Returns Returns
------- -------
data: dict data: dict
""" """
users = {self.account.title: self.account} users = {self.account.title: self.account}
for user in self.account.users(): for user in self.account.users():
users[user.title] = user users[user.title] = user
return users return users
def all_sections(self): def all_sections(self):
"""All sections from all owned servers """Get all sections from all owned servers.
Returns Returns
------- -------
data: dict data: dict
""" """
data = {} data = {}
servers = self.admin_servers() servers = self.admin_servers()
@ -235,21 +241,23 @@ class Plex:
connect = server.connect() connect = server.connect()
sections = {section.title: section for section in connect.library.sections()} sections = {section.title: section for section in connect.library.sections()}
data[name] = sections data[name] = sections
return data return data
def users_access(self): def users_access(self):
"""Users access across all owned servers """Get users access across all owned servers.
Returns Returns
------- -------
data: dict data: dict
""" """
all_users = self.all_users().values() all_users = self.all_users().values()
admin_servers = self.admin_servers() admin_servers = self.admin_servers()
all_sections = self.all_sections() all_sections = self.all_sections()
data = {self.account.title: {"account": self.account}} data = {self.account.title: {"account": self.account}}
for user in all_users: for user in all_users:
if not data.get(user.title): if not data.get(user.title):
servers = [] servers = []
@ -257,7 +265,7 @@ class Plex:
if admin_servers.get(server.name): if admin_servers.get(server.name):
access = {} access = {}
sections = {section.title: section for section in server.sections() sections = {section.title: section for section in server.sections()
if section.shared == True} if section.shared is True}
access['server'] = {server.name: admin_servers.get(server.name)} access['server'] = {server.name: admin_servers.get(server.name)}
access['sections'] = sections access['sections'] = sections
servers += [access] servers += [access]
@ -278,7 +286,8 @@ class Plex:
def connect_to_server(server_obj, user_account): def connect_to_server(server_obj, user_account):
"""Find server url and connect using user token """Find server url and connect using user token.
Parameters Parameters
---------- ----------
server_obj: class server_obj: class
@ -287,10 +296,11 @@ def connect_to_server(server_obj, user_account):
Returns Returns
------- -------
user_connection.server: class user_connection.server: class
""" """
server_name = server_obj.name server_name = server_obj.name
user = user_account.title user = user_account.title
print('Connecting {} to {}...'.format(user, server_name)) print('Connecting {} to {}...'.format(user, server_name))
server_connection = server_obj.connect() server_connection = server_obj.connect()
baseurl = server_connection._baseurl.split('.') baseurl = server_connection._baseurl.split('.')
@ -300,14 +310,15 @@ def connect_to_server(server_obj, user_account):
token = PLEX_TOKEN token = PLEX_TOKEN
else: else:
token = user_account.get_token(server_connection.machineIdentifier) token = user_account.get_token(server_connection.machineIdentifier)
user_connection = Plex(url=url, token=token) user_connection = Plex(url=url, token=token)
return user_connection.server return user_connection.server
def check_users_access(access, user, server_name, libraries=None): def check_users_access(access, user, server_name, libraries=None):
"""Check user's access to server. If allowed connect. """Check user's access to server. If allowed connect.
Parameters Parameters
---------- ----------
access: dict access: dict
@ -318,6 +329,7 @@ def check_users_access(access, user, server_name, libraries=None):
Returns Returns
------- -------
server_connection: class server_connection: class
""" """
try: try:
_user = access.get(user) _user = access.get(user)
@ -333,7 +345,7 @@ def check_users_access(access, user, server_name, libraries=None):
if library_check: if library_check:
server_connection = connect_to_server(server_obj, _user['account']) server_connection = connect_to_server(server_obj, _user['account'])
return server_connection return server_connection
elif not library_check: elif not library_check:
print("User does not have access to this library.") print("User does not have access to this library.")
# Not syncing by libraries # Not syncing by libraries
@ -349,7 +361,8 @@ def check_users_access(access, user, server_name, libraries=None):
def sync_watch_status(watched, section, accountTo, userTo, same_server=False): def sync_watch_status(watched, section, accountTo, userTo, same_server=False):
""" """Sync watched status between two users.
Parameters Parameters
---------- ----------
watched: list watched: list
@ -362,10 +375,12 @@ def sync_watch_status(watched, section, accountTo, userTo, same_server=False):
User's server class of sync to user User's server class of sync to user
same_server: bool same_server: bool
Are serverFrom and serverTo the same Are serverFrom and serverTo the same
""" """
print('Marking watched...') print('Marking watched...')
sectionTo = accountTo.library.section(section) sectionTo = accountTo.library.section(section)
for item in watched: for item in watched:
print(item)
try: try:
if same_server: if same_server:
fetch_check = sectionTo.fetchItem(item.ratingKey) fetch_check = sectionTo.fetchItem(item.ratingKey)
@ -386,7 +401,7 @@ def sync_watch_status(watched, section, accountTo, userTo, same_server=False):
fetch_check.markWatched() fetch_check.markWatched()
title = fetch_check._prettyfilename() title = fetch_check._prettyfilename()
print("Synced watched status of {} to account {}...".format(title, userTo)) print("Synced watched status of {} to account {}...".format(title, userTo))
except Exception as e: except Exception as e:
print(e) print(e)
pass pass
@ -406,11 +421,11 @@ if __name__ == '__main__':
requiredNamed.add_argument('--userTo', nargs='*', metavar='user=server', required=True, requiredNamed.add_argument('--userTo', nargs='*', metavar='user=server', required=True,
type=lambda kv: kv.split("="), type=lambda kv: kv.split("="),
help='Select user and server to sync to.') help='Select user and server to sync to.')
opts = parser.parse_args() opts = parser.parse_args()
# print(opts) # print(opts)
tautulli_server = '' tautulli_server = ''
libraries = [] libraries = []
all_sections = {} all_sections = {}
watchedFrom = '' watchedFrom = ''
@ -419,15 +434,15 @@ if __name__ == '__main__':
start = 0 start = 0
plex_admin = Plex(PLEX_TOKEN) plex_admin = Plex(PLEX_TOKEN)
plex_access = plex_admin.users_access() plex_access = plex_admin.users_access()
userFrom, serverFrom = opts.userFrom userFrom, serverFrom = opts.userFrom
if serverFrom == "Tautulli": if serverFrom == "Tautulli":
# Create a Tautulli instance # Create a Tautulli instance
tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'), tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'),
apikey=TAUTULLI_APIKEY, apikey=TAUTULLI_APIKEY,
verify_ssl=VERIFY_SSL)) verify_ssl=VERIFY_SSL))
if serverFrom == "Tautulli" and opts.libraries: if serverFrom == "Tautulli" and opts.libraries:
# Pull all libraries from Tautulli # Pull all libraries from Tautulli
_sections = {} _sections = {}
@ -451,19 +466,19 @@ if __name__ == '__main__':
else: else:
print("No matching library name '{}'".format(library)) print("No matching library name '{}'".format(library))
exit() exit()
# If server is Plex and synciing libraries, check access # If server is Plex and synciing libraries, check access
if serverFrom != "Tautulli" and libraries: if serverFrom != "Tautulli" and libraries:
print("Checking {}'s access to {}".format(userFrom, serverFrom)) print("Checking {}'s access to {}".format(userFrom, serverFrom))
watchedFrom = check_users_access(plex_access, userFrom, serverFrom, libraries) watchedFrom = check_users_access(plex_access, userFrom, serverFrom, libraries)
if libraries: if libraries:
print("Finding watched items in libraries...") print("Finding watched items in libraries...")
plexTo = [] plexTo = []
for user, server_name in opts.userTo: for user, server_name in opts.userTo:
plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)]) plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
for _library in libraries: for _library in libraries:
watched_lst = [] watched_lst = []
print("Checking {}'s library: '{}' watch statuses...".format(userFrom, _library.title)) print("Checking {}'s library: '{}' watch statuses...".format(userFrom, _library.title))
@ -484,10 +499,14 @@ if __name__ == '__main__':
# Check library for watched items # Check library for watched items
sectionFrom = watchedFrom.library.section(_library.title) sectionFrom = watchedFrom.library.section(_library.title)
if _library.type == 'show': if _library.type == 'show':
watched_lst = sectionFrom.search(libtype='episode', unwatched=False) for show in sectionFrom.all():
for episode in show.episodes():
if episode.isWatched:
watched_lst.append(episode)
else: else:
watched_lst = sectionFrom.search(unwatched=False) for item in sectionFrom.search(unwatched=False):
watched_lst.append(item)
for user in plexTo: for user in plexTo:
username, server = user username, server = user
if server == serverFrom: if server == serverFrom:
@ -497,7 +516,7 @@ if __name__ == '__main__':
elif opts.ratingKey and serverFrom == "Tautulli": elif opts.ratingKey and serverFrom == "Tautulli":
plexTo = [] plexTo = []
watched_item = [] watched_item = []
if userFrom != "Tautulli": if userFrom != "Tautulli":
print("Request manually triggered to update watch status") print("Request manually triggered to update watch status")
tt_watched = tautulli_server.get_watched_history(user=userFrom, rating_key=opts.ratingKey) tt_watched = tautulli_server.get_watched_history(user=userFrom, rating_key=opts.ratingKey)
@ -506,40 +525,18 @@ if __name__ == '__main__':
else: else:
print("Rating Key {} was not reported as watched in Tautulli for user {}".format(opts.ratingKey, userFrom)) print("Rating Key {} was not reported as watched in Tautulli for user {}".format(opts.ratingKey, userFrom))
exit() exit()
elif userFrom == "Tautulli": elif userFrom == "Tautulli":
print("Request from Tautulli notification agent to update watch status") print("Request from Tautulli notification agent to update watch status")
watched_item = Metadata(tautulli_server.get_metadata(opts.ratingKey)) watched_item = Metadata(tautulli_server.get_metadata(opts.ratingKey))
for user, server_name in opts.userTo: for user, server_name in opts.userTo:
# Check access and connect # Check access and connect
plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)]) plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
for user in plexTo: for user in plexTo:
username, server = user username, server = user
sync_watch_status([watched_item], watched_item.libraryName, server, username) sync_watch_status([watched_item], watched_item.libraryName, server, username)
elif opts.ratingKey and serverFrom != "Tautulli":
plexTo = []
watched_item = []
if userFrom != "Tautulli":
print("Request manually triggered to update watch status")
watchedFrom = check_users_access(plex_access, userFrom, serverFrom)
watched_item = watchedFrom.fetchItem(opts.ratingKey)
if watched_item.type in ["episode", "movie"]:
if not watched_item.isWatched:
print("Rating Key {} was not reported as watched in Plex for user {}".format(opts.ratingKey,
userFrom))
exit()
for user, server_name in opts.userTo:
# Check access and connect
plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
for user in plexTo:
username, server = user
sync_watch_status([watched_item], watched_item.libraryName, server, username)
else: else:
print("You aren't using this script correctly... bye!") print("You aren't using this script correctly... bye!")

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" # -*- coding: utf-8 -*-
Description: Sync Tautulli friendly names with Ombi aliases (Tautulli as master)
"""Sync Tautulli friendly names with Ombi aliases (Tautulli as master).
Author: DirtyCajunRice Author: DirtyCajunRice
Requires: requests, python3.6+ Requires: requests, python3.6+
""" """