Merge remote-tracking branch 'origin/master'

This commit is contained in:
blacktwin 2023-11-25 12:33:09 -05:00
commit 5bcdf4fe43
3 changed files with 159 additions and 38 deletions

View File

@ -338,7 +338,7 @@ Tautulli > Settings > Notification Agents > New Script > Conditions:
- [ ] Set desired conditions - [ ] Set desired conditions
- [ ] Save - [ ] 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 #### Script Arguments
Tautulli > Settings > Notification Agents > New Script > Script Arguments: Tautulli > Settings > Notification Agents > New Script > Script Arguments:

View File

@ -21,8 +21,13 @@
# --rating_key {rating_key} --blur 25 # --rating_key {rating_key} --blur 25
# To add a prefix to the summary (optional string prefix): # To add a prefix to the summary (optional string prefix):
# --rating_key {rating_key} --summary_prefix "** SPOILERS **" # --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): # * 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: # Note:
# * "Use local assets" must be enabled for the library in Plex (Manage Library > Edit > Advanced > Use local assets). # * "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) 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) item = plex.fetchItem(rating_key)
if item.type == 'show': 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] episode_filename = os.path.splitext(os.path.basename(episode_filepath))[0]
if remove: if remove:
# Find image files with the same name as the episode if upload:
for filename in os.listdir(episode_folder): # Unlock and select the first poster
if filename.startswith(episode_filename) and filename.endswith(('.jpg', '.png')): episode.unlockPoster().posters()[0].select()
# Delete the episode artwork image file else:
os.remove(os.path.join(episode_folder, filename)) # 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 # Unlock the summary so it will get updated on refresh
episode.edit(**{'summary.locked': 0}) episode.editSummary(episode.summary, locked=False)
continue continue
if image: if image:
# File path to episode artwork using the same episode file name if upload:
episode_artwork = os.path.splitext(episode_filepath)[0] + os.path.splitext(image)[1] # Upload the image to the episode artwork
# Copy the image to the episode artwork episode.uploadPoster(filepath=image)
shutil.copy2(image, episode_artwork) 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: elif blur:
# File path to episode artwork using the same episode file name # 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) r = requests.get(image_url, stream=True)
if r.status_code == 200: if r.status_code == 200:
r.raw.decode_content = True r.raw.decode_content = True
# Copy the image to the episode artwork if upload:
with open(episode_artwork, 'wb') as f: # Upload the image to the episode artwork
shutil.copyfileobj(r.raw, f) 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): if summary_prefix and not episode.summary.startswith(summary_prefix):
# Use a zero-width space (\u200b) for blank lines # Use a zero-width space (\u200b) for blank lines
episode.edit(**{ episode.editSummary(summary_prefix + '\n\u200b\n' + episode.summary)
'summary.value': summary_prefix + '\n\u200b\n' + episode.summary,
'summary.locked': 1
})
# Refresh metadata for the episode # Refresh metadata for the episode
episode.refresh() episode.refresh()
@ -113,6 +127,7 @@ if __name__ == "__main__":
parser.add_argument('--blur', type=int, default=25) parser.add_argument('--blur', type=int, default=25)
parser.add_argument('--summary_prefix', nargs='?', const='** SPOILERS **') parser.add_argument('--summary_prefix', nargs='?', const='** SPOILERS **')
parser.add_argument('--remove', action='store_true') parser.add_argument('--remove', action='store_true')
parser.add_argument('--upload', action='store_true')
opts = parser.parse_args() opts = parser.parse_args()
plex = PlexServer(PLEX_URL, PLEX_TOKEN) plex = PlexServer(PLEX_URL, PLEX_TOKEN)

View File

@ -4,7 +4,7 @@
''' '''
Description: Automatically merge multi-episode files in Plex into a single entry. Description: Automatically merge multi-episode files in Plex into a single entry.
Author: /u/SwiftPanda16 Author: /u/SwiftPanda16
Requires: plexapi Requires: plexapi, pillow (optional)
Notes: Notes:
* All episodes **MUST** be organized correctly according to Plex's "Multiple Episodes in a Single File". * 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 https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/#toc-4
@ -26,25 +26,44 @@ Usage:
* With renumbering episodes: * With renumbering episodes:
python merge_multiepisodes.py --library "TV Shows" --show "SpongeBob SquarePants" --renumber 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 argparse
import functools
import io
import math
import os import os
import requests
from collections import defaultdict from collections import defaultdict
from plexapi.server import PlexServer from plexapi.server import PlexServer
try:
from PIL import Image, ImageDraw
hasPIL = True
except ImportError:
hasPIL = False
# ## EDIT SETTINGS ## # ## EDIT SETTINGS ##
PLEX_URL = '' PLEX_URL = ''
PLEX_TOKEN = '' PLEX_TOKEN = ''
# Composite Thumb Settings
WIDTH, HEIGHT = 640, 360 # 16:9 aspect ratio
LINE_ANGLE = 25 # degrees
LINE_THICKNESS = 10
# Environmental Variables # Environmental Variables
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) 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) show = plex.library.section(library).get(show)
for season in show.seasons(): for season in show.seasons():
@ -71,27 +90,32 @@ def group_episodes(plex, library, show, renumber):
directors.extend([director.tag for director in episode.directors]) directors.extend([director.tag for director in episode.directors])
if episodes: 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) merge(first, episodes)
first.addWriter(writers, locked=True) first.batchEdits() \
first.addDirector(directors, locked=True) .editTitle(title[:-3]) \
.editSortTitle(titleSort[:-3]) \
edits = { .editSummary(summary[:-2]) \
'title.value': title[:-3], .editContentRating(first.contentRating) \
'title.locked': 1, .editOriginallyAvailable(first.originallyAvailableAt) \
'titleSort.value': titleSort[:-3], .addWriter(writers) \
'titleSort.locked': 1, .addDirector(directors) \
'summary.value': summary[:-2],
'summary.locked': 1,
'originallyAvailableAt.locked': 1,
'contentRating.locked': 1
}
if renumber: if renumber:
edits['index.value'] = index first._edits['index.value'] = index
edits['index.locked'] = 1 first._edits['index.locked'] = 1
first.edit(**edits) first.saveEdits()
def merge(first, episodes): def merge(first, episodes):
@ -99,12 +123,94 @@ def merge(first, episodes):
first._server.query(key, method=first._server._session.put) 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__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--library', required=True) parser.add_argument('--library', required=True)
parser.add_argument('--show', required=True) parser.add_argument('--show', required=True)
parser.add_argument('--renumber', action='store_true') parser.add_argument('--renumber', action='store_true')
parser.add_argument('--composite_thumb', action='store_true')
opts = parser.parse_args() 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) plex = PlexServer(PLEX_URL, PLEX_TOKEN)
group_episodes(plex, **vars(opts)) group_episodes(plex, **vars(opts))