From 5f99b37cfedb3257a5f07ca25a6611bd42ad88b3 Mon Sep 17 00:00:00 2001 From: JonnyWong16 Date: Sun, 27 Oct 2019 15:58:42 -0700 Subject: [PATCH] Add Tautulli triggered scripts --- utility/add_label_recently_added.py | 79 +++++++++++++++++++ utility/hide_episode_spoilers.py | 110 +++++++++++++++++++++++++++ utility/mark_multiepisode_watched.py | 46 +++++++++++ utility/recently_added_collection.py | 50 ++++++++++++ utility/spoilers.png | Bin 0 -> 11599 bytes 5 files changed, 285 insertions(+) create mode 100644 utility/add_label_recently_added.py create mode 100644 utility/hide_episode_spoilers.py create mode 100644 utility/mark_multiepisode_watched.py create mode 100644 utility/recently_added_collection.py create mode 100644 utility/spoilers.png diff --git a/utility/add_label_recently_added.py b/utility/add_label_recently_added.py new file mode 100644 index 0000000..e3bfad7 --- /dev/null +++ b/utility/add_label_recently_added.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Description: Automatically add a label to recently added items in your Plex library +# Author: /u/SwiftPanda16 +# Requires: requests +# Tautulli script trigger: +# * Notify on recently added +# Tautulli script conditions: +# * Filter which media to add labels to using conditions. Examples: +# [ Media Type | is | movie ] +# [ Show Name | is | Game of Thrones ] +# [ Album Name | is | Reputation ] +# [ Video Resolution | is | 4k ] +# [ Genre | contains | horror ] +# Tautulli script arguments: +# * Recently Added: +# --title {title} --section_id {section_id} --media_type {media_type} --rating_key {rating_key} --parent_rating_key {parent_rating_key} --grandparent_rating_key {grandparent_rating_key} --label "Label" + +import argparse +import os +import requests + + +### OVERRIDES - ONLY EDIT IF RUNNING SCRIPT WITHOUT TAUTULLI ### + +PLEX_URL = '' +PLEX_TOKEN = '' + + +### CODE BELOW ### + +PLEX_URL = PLEX_URL or os.getenv('PLEX_URL', PLEX_URL) +PLEX_TOKEN = PLEX_TOKEN or os.getenv('PLEX_TOKEN', PLEX_TOKEN) + +MEDIA_TYPES_PARENT_VALUES = {'movie': 1, 'show': 2, 'season': 2, 'episode': 2, 'album': 9, 'track': 9} + + +def add_label(media_type_value, rating_key, section_id, label): + headers = {'X-Plex-Token': PLEX_TOKEN} + params = {'type': media_type_value, + 'id': rating_key, + 'label.locked': 1, + 'label[0].tag.tag': label + } + + url = '{base_url}/library/sections/{section_id}/all'.format(base_url=PLEX_URL, section_id=section_id) + r = requests.put(url, headers=headers, params=params) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--title', required=True) + parser.add_argument('--section_id', required=True) + parser.add_argument('--media_type', required=True) + parser.add_argument('--rating_key', required=True) + parser.add_argument('--parent_rating_key', required=True) + parser.add_argument('--grandparent_rating_key', required=True) + parser.add_argument('--label', required=True) + opts = parser.parse_args() + + if opts.media_type not in MEDIA_TYPES_PARENT_VALUES: + print("Cannot add label to '{opts.title}': Invalid media type '{opts.media_type}'".format(opts=opts)) + else: + media_type_value = MEDIA_TYPES_PARENT_VALUES[opts.media_type] + rating_key = '' + + if opts.media_type in ('movie', 'show', 'album'): + rating_key = opts.rating_key + elif opts.media_type in ('season', 'track'): + rating_key = opts.parent_rating_key + elif opts.media_type in ('episode'): + rating_key = opts.grandparent_rating_key + + if rating_key and rating_key.isdigit(): + add_label(media_type_value, int(rating_key), opts.section_id, opts.label) + print("The label '{opts.label}' was added to '{opts.title}' ({rating_key}).".format(opts=opts, rating_key=rating_key)) + else: + print("Cannot add label to '{opts.title}': Invalid rating key '{rating_key}'".format(opts=opts, rating_key=rating_key)) diff --git a/utility/hide_episode_spoilers.py b/utility/hide_episode_spoilers.py new file mode 100644 index 0000000..7df4d7c --- /dev/null +++ b/utility/hide_episode_spoilers.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Description: Automatically change episode artwork in Plex to hide spoilers. +# Author: /u/SwiftPanda16 +# Requires: plexapi, requests +# Tautulli script trigger: +# * Notify on recently added +# * Notify on watched (optional - to remove the artwork after being watched) +# Tautulli script conditions: +# * Condition {1}: +# [ Media Type | is | episode ] +# * Condition {2} (optional): +# [ Library Name | is | DVR ] +# [ Show Namme | is | Game of Thrones ] +# Tautulli script arguments: +# * Recently Added: +# To use an image file (can be image in the same directory as this script, or full path to an image): +# --rating_key {rating_key} --file {file} --image spoilers.png +# To blur the episode artwork (optional blur in pixels): +# --rating_key {rating_key} --file {file} --blur 25 +# * Watched (optional): +# --rating_key {rating_key} --file {file} --remove + +import argparse +import os +import requests +import shutil +from plexapi.server import PlexServer + +TAUTULLI_URL = '' +TAUTULLI_APIKEY = '' +PLEX_URL = '' +PLEX_TOKEN = '' + +# Environmental Variables +TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL) +TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY) +PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) +PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) + + +def get_blurred_image(rating_key, blur=25): + params = {'apikey': TAUTULLI_APIKEY, + 'cmd': 'pms_image_proxy', + 'img': '/library/metadata/{}/thumb'.format(rating_key), + 'width': 545, + 'height': 307, + 'opacity': 100, + 'background': '000000', + 'blur': blur, + 'img_format': 'png', + 'fallback': 'art' + } + + r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=params, stream=True) + if r.status_code == 200: + r.raw.decode_content = True + return r.raw + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--rating_key', required=True, type=int) + parser.add_argument('--file', required=True) + parser.add_argument('--image') + parser.add_argument('--blur', type=int, default=25) + parser.add_argument('--remove', action='store_true') + opts = parser.parse_args() + + if opts.image: + # File path to episode artwork using the same episode file name + episode_artwork = os.path.splitext(opts.file)[0] + os.path.splitext(opts.image)[1] + + # Copy the image to the episode artwork + shutil.copy2(opts.image, episode_artwork) + + # Refresh metadata for the TV show + plex = PlexServer(PLEX_URL, PLEX_TOKEN) + plex.fetchItem(opts.rating_key).show().refresh() + + elif opts.blur: + # File path to episode artwork using the same episode file name + episode_artwork = os.path.splitext(opts.file)[0] + '.png' + + # Get the blurred artwork from Tautulli + blurred_artwork = get_blurred_image(opts.rating_key, opts.blur) + if blurred_artwork: + # Copy the image to the episode artwork + with open(episode_artwork, 'wb') as f: + shutil.copyfileobj(blurred_artwork, f) + + # Refresh metadata for the TV show + plex = PlexServer(PLEX_URL, PLEX_TOKEN) + plex.fetchItem(opts.rating_key).show().refresh() + + elif opts.remove: + # File path to episode artwork using the same episode file name without extension + episode_path = os.path.dirname(opts.file) + episode_filename = os.path.splitext(os.path.basename(opts.file))[0] + + # Find image files with the same name as the episode + for filename in os.listdir(episode_path): + if filename.startswith(episode_filename) and filename.endswith(('.jpg', '.png')): + # Delete the episode artwork image file + os.remove(os.path.join(episode_path, filename)) + + # Refresh metadata for the TV show + plex = PlexServer(PLEX_URL, PLEX_TOKEN) + plex.fetchItem(opts.rating_key).show().refresh() diff --git a/utility/mark_multiepisode_watched.py b/utility/mark_multiepisode_watched.py new file mode 100644 index 0000000..8878a8d --- /dev/null +++ b/utility/mark_multiepisode_watched.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Description: Automatically mark a multi-episode file as watched in Plex. +# Author: /u/SwiftPanda16 +# Requires: plexapi +# Tautulli script trigger: +# * Notify on watched +# Tautulli script conditions: +# * Condition {1}: +# [ Media Type | is | episode ] +# * Condition {2} (optional): +# [ Username | is | username ] +# Tautulli script arguments: +# * Watched: +# --rating_key {rating_key} --filename {filename} + +import argparse +import os +from plexapi.server import PlexServer + +PLEX_URL = '' +PLEX_TOKEN = '' + +# Environmental Variables +PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) +PLEX_USER_TOKEN = os.getenv('PLEX_USER_TOKEN', PLEX_TOKEN) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--rating_key', required=True, type=int) + parser.add_argument('--filename', required=True) + opts = parser.parse_args() + + plex = PlexServer(PLEX_URL, PLEX_USER_TOKEN) + + for episode in plex.fetchItem(opts.rating_key).season().episodes(): + if episode.ratingKey == opts.rating_key: + continue + if any(opts.filename in part.file for media in episode.media for part in media.parts): + print("Marking multi-episode file '{grandparentTitle} - S{parentIndex}E{index}' as watched.".format( + grandparentTitle=episode.grandparentTitle.encode('UTF-8'), + parentIndex=str(episode.parentIndex).zfill(2), + index=str(episode.index).zfill(2))) + episode.markWatched() diff --git a/utility/recently_added_collection.py b/utility/recently_added_collection.py new file mode 100644 index 0000000..5b429e7 --- /dev/null +++ b/utility/recently_added_collection.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Description: Automatically add a movie to a collection based on release date. +# Author: /u/SwiftPanda16 +# Requires: plexapi +# Tautulli script trigger: +# * Notify on recently added +# Tautulli script conditions: +# * Filter which media to add to collection. +# [ Media Type | is | movie ] +# [ Library Name | is | Movies ] +# Tautulli script arguments: +# * Recently Added: +# --rating_key {rating_key} --collection "New Releases" --days 180 + +import argparse +import os +from datetime import datetime, timedelta +from plexapi.server import PlexServer + +PLEX_URL = '' +PLEX_TOKEN = '' + +# Environmental Variables +PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) +PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--rating_key', required=True, type=int) + parser.add_argument('--collection', required=True) + parser.add_argument('--days', required=True, type=int) + opts = parser.parse_args() + + threshold_date = datetime.now() - timedelta(days=opts.days) + + plex = PlexServer(PLEX_URL, PLEX_TOKEN) + + movie = plex.fetchItem(opts.rating_key) + + if movie.originallyAvailableAt >= threshold_date: + movie.addCollection(opts.collection) + print("Added collection '{}' to '{}'.".format(opts.collection, movie.title.encode('UTF-8'))) + + for m in movie.section().search(collection=opts.collection): + if m.originallyAvailableAt < threshold_date: + m.removeCollection(opts.collection) + print("Removed collection '{}' from '{}'.".format(opts.collection, m.title.encode('UTF-8'))) diff --git a/utility/spoilers.png b/utility/spoilers.png new file mode 100644 index 0000000000000000000000000000000000000000..df6f44f473818aa564a0e860bb4688c50f0858ed GIT binary patch literal 11599 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