From a04b1a3c32874f5e9a3683907c1e67978b076c7f Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:37:33 -0800 Subject: [PATCH 1/4] Add upload option to hide_episode_spoilers.py --- utility/hide_episode_spoilers.py | 53 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/utility/hide_episode_spoilers.py b/utility/hide_episode_spoilers.py index 6acb5d6..0d800e1 100644 --- a/utility/hide_episode_spoilers.py +++ b/utility/hide_episode_spoilers.py @@ -21,8 +21,13 @@ # --rating_key {rating_key} --blur 25 # To add a prefix to the summary (optional string prefix): # --rating_key {rating_key} --summary_prefix "** SPOILERS **" +# To upload the episode artwork instead of creating a local asset (optional, for when the script cannot access the media folder): +# --rating_key {rating_key} --blur 25 --upload # * Watched (optional): -# --rating_key {rating_key} --remove +# To remove the local asset episode artwork: +# --rating_key {rating_key} --remove +# To remove the uploaded episode artwork +# --rating_key {rating_key} --remove --upload # Note: # * "Use local assets" must be enabled for the library in Plex (Manage Library > Edit > Advanced > Use local assets). @@ -40,7 +45,7 @@ PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) -def modify_episode_artwork(plex, rating_key, image=None, blur=None, summary_prefix=None, remove=False): +def modify_episode_artwork(plex, rating_key, image=None, blur=None, summary_prefix=None, remove=False, upload=False): item = plex.fetchItem(rating_key) if item.type == 'show': @@ -61,21 +66,29 @@ def modify_episode_artwork(plex, rating_key, image=None, blur=None, summary_pref episode_filename = os.path.splitext(os.path.basename(episode_filepath))[0] if remove: - # Find image files with the same name as the episode - for filename in os.listdir(episode_folder): - if filename.startswith(episode_filename) and filename.endswith(('.jpg', '.png')): - # Delete the episode artwork image file - os.remove(os.path.join(episode_folder, filename)) + if upload: + # Unlock and select the first poster + episode.unlockPoster().posters()[0].select() + else: + # Find image files with the same name as the episode + for filename in os.listdir(episode_folder): + if filename.startswith(episode_filename) and filename.endswith(('.jpg', '.png')): + # Delete the episode artwork image file + os.remove(os.path.join(episode_folder, filename)) # Unlock the summary so it will get updated on refresh - episode.edit(**{'summary.locked': 0}) + episode.editSummary(episode.summary, locked=False) continue if image: - # File path to episode artwork using the same episode file name - episode_artwork = os.path.splitext(episode_filepath)[0] + os.path.splitext(image)[1] - # Copy the image to the episode artwork - shutil.copy2(image, episode_artwork) + if upload: + # Upload the image to the episode artwork + episode.uploadPoster(filepath=image) + else: + # File path to episode artwork using the same episode file name + episode_artwork = os.path.splitext(episode_filepath)[0] + os.path.splitext(image)[1] + # Copy the image to the episode artwork + shutil.copy2(image, episode_artwork) elif blur: # File path to episode artwork using the same episode file name @@ -91,16 +104,17 @@ def modify_episode_artwork(plex, rating_key, image=None, blur=None, summary_pref r = requests.get(image_url, stream=True) if r.status_code == 200: r.raw.decode_content = True - # Copy the image to the episode artwork - with open(episode_artwork, 'wb') as f: - shutil.copyfileobj(r.raw, f) + if upload: + # Upload the image to the episode artwork + episode.uploadPoster(filepath=r.raw) + else: + # Copy the image to the episode artwork + with open(episode_artwork, 'wb') as f: + shutil.copyfileobj(r.raw, f) if summary_prefix and not episode.summary.startswith(summary_prefix): # Use a zero-width space (\u200b) for blank lines - episode.edit(**{ - 'summary.value': summary_prefix + '\n\u200b\n' + episode.summary, - 'summary.locked': 1 - }) + episode.editSummary(summary_prefix + '\n\u200b\n' + episode.summary) # Refresh metadata for the episode episode.refresh() @@ -113,6 +127,7 @@ if __name__ == "__main__": parser.add_argument('--blur', type=int, default=25) parser.add_argument('--summary_prefix', nargs='?', const='** SPOILERS **') parser.add_argument('--remove', action='store_true') + parser.add_argument('--upload', action='store_true') opts = parser.parse_args() plex = PlexServer(PLEX_URL, PLEX_TOKEN) From f28a52d727f395fe516fd822bb9cb48da68582f4 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Fri, 17 Feb 2023 11:00:46 -0800 Subject: [PATCH 2/4] Use batchEdits() method for merge_multiepisodes --- utility/merge_multiepisodes.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/utility/merge_multiepisodes.py b/utility/merge_multiepisodes.py index c8e6dc7..0388bd9 100644 --- a/utility/merge_multiepisodes.py +++ b/utility/merge_multiepisodes.py @@ -73,25 +73,20 @@ def group_episodes(plex, library, show, renumber): if episodes: merge(first, episodes) - first.addWriter(writers, locked=True) - first.addDirector(directors, locked=True) - - edits = { - 'title.value': title[:-3], - 'title.locked': 1, - 'titleSort.value': titleSort[:-3], - 'titleSort.locked': 1, - 'summary.value': summary[:-2], - 'summary.locked': 1, - 'originallyAvailableAt.locked': 1, - 'contentRating.locked': 1 - } + first.batchEdits() \ + .editTitle(title[:-3]) \ + .editSortTitle(titleSort[:-3]) \ + .editSummary(summary[:-2]) \ + .editContentRating(first.contentRating) \ + .editOriginallyAvailable(first.originallyAvailableAt) \ + .addWriter(writers) \ + .addDirector(directors) \ if renumber: - edits['index.value'] = index - edits['index.locked'] = 1 + first._edits['index.value'] = index + first._edits['index.locked'] = 1 - first.edit(**edits) + first.saveEdits() def merge(first, episodes): From 02c09389dd04f57a4627b75d2ece055b4117f0dd Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 19 Feb 2023 14:26:45 -0800 Subject: [PATCH 3/4] Add composite_thumb option to merge_multiepisodes --- utility/merge_multiepisodes.py | 115 ++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/utility/merge_multiepisodes.py b/utility/merge_multiepisodes.py index 0388bd9..7ae93b4 100644 --- a/utility/merge_multiepisodes.py +++ b/utility/merge_multiepisodes.py @@ -4,7 +4,7 @@ ''' Description: Automatically merge multi-episode files in Plex into a single entry. Author: /u/SwiftPanda16 -Requires: plexapi +Requires: plexapi, pillow (optional) Notes: * All episodes **MUST** be organized correctly according to Plex's "Multiple Episodes in a Single File". https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/#toc-4 @@ -26,25 +26,44 @@ Usage: * With renumbering episodes: python merge_multiepisodes.py --library "TV Shows" --show "SpongeBob SquarePants" --renumber + + * With renumbering episodes and composite thumb: + python merge_multiepisodes.py --library "TV Shows" --show "SpongeBob SquarePants" --renumber --composite-thumb ''' import argparse +import functools +import io +import math import os +import requests from collections import defaultdict from plexapi.server import PlexServer +try: + from PIL import Image, ImageDraw + hasPIL = True +except ImportError: + hasPIL = False + # ## EDIT SETTINGS ## PLEX_URL = '' PLEX_TOKEN = '' +# Composite Thumb Settings +WIDTH, HEIGHT = 640, 360 # 16:9 aspect ratio +LINE_ANGLE = 25 # degrees +LINE_THICKNESS = 10 + + # Environmental Variables PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) -def group_episodes(plex, library, show, renumber): +def group_episodes(plex, library, show, renumber, composite_thumb): show = plex.library.section(library).get(show) for season in show.seasons(): @@ -71,6 +90,16 @@ def group_episodes(plex, library, show, renumber): directors.extend([director.tag for director in episode.directors]) if episodes: + if composite_thumb: + firstImgFile = download_image( + plex.transcodeImage(first.thumbUrl, width=WIDTH, height=HEIGHT) + ) + lastImgFile = download_image( + plex.transcodeImage(episodes[-1].thumbUrl, width=WIDTH, height=HEIGHT) + ) + compImgFile = create_composite_thumb(firstImgFile, lastImgFile) + first.uploadPoster(filepath=compImgFile) + merge(first, episodes) first.batchEdits() \ @@ -94,12 +123,94 @@ def merge(first, episodes): first._server.query(key, method=first._server._session.put) +def download_image(url): + r = requests.get(url, stream=True) + r.raw.decode_content = True + return r.raw + + +def create_composite_thumb(firstImgFile, lastImgFile): + mask, line = create_masks() + + # Open and crop first image + firstImg = Image.open(firstImgFile) + width, height = firstImg.size + firstImg = firstImg.crop( + ( + (width - WIDTH) // 2, + (height - HEIGHT) // 2, + (width + WIDTH) // 2, + (height + HEIGHT) // 2 + ) + ) + + # Open and crop last image + lastImg = Image.open(lastImgFile) + width, height = lastImg.size + lastImg = lastImg.crop( + ( + (width - WIDTH) // 2, + (height - HEIGHT) // 2, + (width + WIDTH) // 2, + (height + HEIGHT) // 2 + ) + ) + + # Create composite image + comp = Image.composite(line, Image.composite(firstImg, lastImg, mask), line) + + # Return composite image as file-like object + compImgFile = io.BytesIO() + comp.save(compImgFile, format='jpeg') + compImgFile.seek(0) + return compImgFile + + +@functools.lru_cache(maxsize=None) +def create_masks(): + scale = 3 # For line anti-aliasing + offset = HEIGHT // 2 * math.tan(LINE_ANGLE * math.pi / 180) + + # Create diagonal mask + mask = Image.new('L', (WIDTH, HEIGHT), 0) + draw = ImageDraw.Draw(mask) + draw.polygon( + ( + (0, 0), + (WIDTH // 2 + offset, 0), + (WIDTH // 2 - offset, HEIGHT), + (0, HEIGHT) + ), + fill=255 + ) + + # Create diagonal line (use larger image then scale down with anti-aliasing) + line = Image.new('L', (scale * WIDTH, scale * HEIGHT), 0) + draw = ImageDraw.Draw(line) + draw.line( + ( + (scale * (WIDTH // 2 + offset), -scale), + (scale * (WIDTH // 2 - offset), scale * (HEIGHT + 1)) + ), + fill=255, + width=scale * LINE_THICKNESS + ) + line = line.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS) + + return mask, line + + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--library', required=True) parser.add_argument('--show', required=True) parser.add_argument('--renumber', action='store_true') + parser.add_argument('--composite_thumb', action='store_true') opts = parser.parse_args() + if opts.composite_thumb and not hasPIL: + print('PIL is not installed. Please install `pillow` to create composite thumbnails.') + exit(1) + plex = PlexServer(PLEX_URL, PLEX_TOKEN) group_episodes(plex, **vars(opts)) From a3d3e6c295a2b4eda243f0b500c9ca618e4adb1d Mon Sep 17 00:00:00 2001 From: JohnnyGrey86 Date: Sun, 5 Mar 2023 14:16:38 -0600 Subject: [PATCH 4/4] Update README.md Changed URL for Tautulli Custom Notification Conditions wiki article. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34c8aa2..8385c31 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ Tautulli > Settings > Notification Agents > New Script > Conditions: - [ ] Set desired conditions - [ ] Save -For more information on Tautulli conditions see [here](https://github.com/Tautulli/Tautulli-Wiki/wiki/Custom-Notification-Conditions) +For more information on Tautulli conditions see [here](https://github.com/Tautulli/Tautulli/wiki/Custom-Notification-Conditions) #### Script Arguments Tautulli > Settings > Notification Agents > New Script > Script Arguments: