From e9690f29b51fba35328f8b40704df51fbef7055d Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Mon, 13 Jun 2022 09:34:29 -0600 Subject: [PATCH 1/9] Fix for season poster color specification Fixes #174 --- modules/SeasonPosterSet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/SeasonPosterSet.py b/modules/SeasonPosterSet.py index c242c24f7..cf7d9ee28 100755 --- a/modules/SeasonPosterSet.py +++ b/modules/SeasonPosterSet.py @@ -85,7 +85,7 @@ def __read_font(self, font_config: dict) -> None: self.valid = False if (color := font_config.get('color')) != None: - if (not isinstance(value, str) + if (not isinstance(color, str) or not bool(match('^#[a-fA-F0-9]{6}$', color))): log.error(f'Font color "{color}" is invalid, specify as ' f'"#xxxxxx"') From 9c07fca0512582df0986d2b2868d9cfd0a305595 Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Mon, 13 Jun 2022 15:21:52 -0600 Subject: [PATCH 2/9] Default to no color output in docker container --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6e9e1f1a3..d2c09fd40 100755 --- a/Dockerfile +++ b/Dockerfile @@ -43,5 +43,5 @@ RUN pip3 install --no-cache-dir --upgrade pipenv; \ RUN rm -f Pipfile Pipfile.lock requirements.txt # Entrypoint -CMD ["python3", "main.py", "--run"] +CMD ["python3", "main.py", "--run", "--no-color"] ENTRYPOINT ["bash", "./start.sh"] From 82afb4ad05fb71e8a02bbcb5a448cb824123f6ef Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Mon, 13 Jun 2022 15:22:22 -0600 Subject: [PATCH 3/9] Create season posters in archive --- modules/Manager.py | 4 ++-- modules/SeasonPosterSet.py | 3 ++- modules/Show.py | 16 +++++++++++++--- modules/ShowArchive.py | 15 +++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/modules/Manager.py b/modules/Manager.py index 3579fb996..d541b487f 100755 --- a/modules/Manager.py +++ b/modules/Manager.py @@ -189,8 +189,8 @@ def create_season_posters(self) -> None: """Create season posters for all shows known to this Manager.""" # For each show in the Manager, create its posters - for show in (pbar := tqdm(self.shows, desc='Creating season posters', - **TQDM_KWARGS)): + for show in (pbar := tqdm(self.shows + self.archives, + desc='Creating season posters',**TQDM_KWARGS)): show.create_season_posters() diff --git a/modules/SeasonPosterSet.py b/modules/SeasonPosterSet.py index cf7d9ee28..71c0e3532 100755 --- a/modules/SeasonPosterSet.py +++ b/modules/SeasonPosterSet.py @@ -56,7 +56,8 @@ def __init__(self, episode_map: 'EpisodeMap', source_directory: Path, # If posters aren't enabled, skip rest of parsing poster_config = {} if poster_config is None else poster_config - if not poster_config.get('create', True): + if (self.__media_directory is None + or not poster_config.get('create', True)): return None # Read the font specification diff --git a/modules/Show.py b/modules/Show.py index 7d4f5ec84..8c4561937 100755 --- a/modules/Show.py +++ b/modules/Show.py @@ -1,3 +1,4 @@ +from copy import copy from pathlib import Path from tqdm import tqdm @@ -142,14 +143,23 @@ def __repr__(self) -> str: return f'' - def __copy__(self) -> 'Show': + def _copy_with_modified_media_directory(self, + media_directory: Path) -> 'Show': """ - Copy this Show object into a new (identical) Show. + Recreate this Show object with a modified media directory. + + :param media_directory: Media directory the returned Show object + will utilize. :returns: A newly constructed Show object. """ - return Show(self.series_info.name, self._base_yaml, self.__library_map, + # Modify base yaml to have overriden media_directory attribute + modified_base = copy(self._base_yaml) + modified_base['media_directory'] = str(media_directory.resolve()) + + # Recreate Show object with modified YAML + return Show(self.series_info.name, modified_base, self.__library_map, self.font._Font__font_map, self.source_directory.parent) diff --git a/modules/ShowArchive.py b/modules/ShowArchive.py index a7720a40e..3ce31d25f 100755 --- a/modules/ShowArchive.py +++ b/modules/ShowArchive.py @@ -65,20 +65,23 @@ def __init__(self, archive_directory: 'Path', base_show: 'Show') -> None: # Go through each valid profile for profile_attributes in valid_profiles: - # Create show object for this profile - new_show = copy(base_show) - - # Update media directory + # Get directory name for this profile profile_directory = self.PROFILE_DIRECTORY_MAP[ f'{profile_attributes["seasons"]}-{profile_attributes["font"]}' ] - # For non-standard card classes, modify archive directory name + # For non-standard card classes, modify profile directory name if base_show.card_class.ARCHIVE_NAME != 'standard': profile_directory += f' - {base_show.card_class.ARCHIVE_NAME}' + # Get modified media directory within the archive directory temp_path = archive_directory / base_show.series_info.legal_path - new_show.media_directory = temp_path / profile_directory + new_media_directory = temp_path / profile_directory + + # Create modified Show object for this profile + new_show = base_show._copy_with_modified_media_directory( + new_media_directory + ) # Convert this new show's profile new_show.profile.convert_profile( From f122827d602967f3299ddc8aa9c296b75b0a04db Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Mon, 13 Jun 2022 15:37:52 -0600 Subject: [PATCH 4/9] Handle runtime permission errors --- main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 00aa12553..aa1881a34 100755 --- a/main.py +++ b/main.py @@ -126,9 +126,13 @@ def run(): RemoteFile.LOADED.truncate() # Create Manager, run, and write missing report - tcm = Manager() - tcm.run() - tcm.report_missing(args.missing) + try: + tcm = Manager() + tcm.run() + tcm.report_missing(args.missing) + except PermissionError as error: + log.critical(f'Invalid permissions - {error}') + exit(1) # Run immediately if specified if args.run: From f54ea9f952f020c5c18adda0b5793cbd8d4f0211 Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Tue, 14 Jun 2022 18:25:44 -0600 Subject: [PATCH 5/9] Handle bad Plex tokens Critically error if the provided Plex X-Token is bad --- modules/PlexInterface.py | 10 +++++++--- modules/StandardTitleCard.py | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/PlexInterface.py b/modules/PlexInterface.py index b7b5476bc..32fe56a8d 100755 --- a/modules/PlexInterface.py +++ b/modules/PlexInterface.py @@ -1,10 +1,9 @@ from pathlib import Path -from plexapi.server import PlexServer, NotFound +from plexapi.server import PlexServer, NotFound, Unauthorized from tenacity import retry, stop_after_attempt, wait_fixed, wait_exponential from tinydb import TinyDB, where from tqdm import tqdm -from yaml import safe_load from modules.Debug import log, TQDM_KWARGS @@ -34,7 +33,12 @@ def __init__(self, url: str, x_plex_token: str=None) -> None: """ # Create PlexServer object with these arguments - self.__server = PlexServer(url, x_plex_token) + try: + self.__token = x_plex_token + self.__server = PlexServer(url, x_plex_token) + except Unauthorized: + log.critical(f'Invalid Plex Token "{x_plex_token}"') + exit(1) # Create/read loaded card database self.__db = TinyDB(self.LOADED_DB) diff --git a/modules/StandardTitleCard.py b/modules/StandardTitleCard.py index f65fa6b21..ba778b958 100755 --- a/modules/StandardTitleCard.py +++ b/modules/StandardTitleCard.py @@ -381,8 +381,8 @@ def _combine_titled_image_series_count_text(self, titled_image: Path, @staticmethod def is_custom_font(font: 'Font') -> bool: """ - Determines whether the given font characteristics constitute a default - or custom font. + Determine whether the given font characteristics constitute a default or + custom font. :param font: The Font being evaluated. @@ -403,7 +403,7 @@ def is_custom_font(font: 'Font') -> bool: def is_custom_season_titles(custom_episode_map: bool, episode_text_format: str) -> bool: """ - Determines whether the given attributes constitute custom or generic + Determine whether the given attributes constitute custom or generic season titles. :param custom_episode_map: Whether the EpisodeMap was From 4c70a84758dad4180a24d4133b67c179528aac17 Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Tue, 14 Jun 2022 18:29:22 -0600 Subject: [PATCH 6/9] Properly get source images from unauthorized Plex servers - Fix for getting source images from Plex on unauthorized devices - Fixes #186 --- modules/PlexInterface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/PlexInterface.py b/modules/PlexInterface.py index 32fe56a8d..751880cd6 100755 --- a/modules/PlexInterface.py +++ b/modules/PlexInterface.py @@ -323,7 +323,8 @@ def get_source_image(self, library_name: str, series_info: 'SeriesInfo', episode=episode_info.episode_number ) - return f'{self.__server._baseurl}{plex_episode.thumb}' + return (f'{self.__server._baseurl}{plex_episode.thumb}' + f'?X-Plex-Token={self.__token}') except NotFound: # Episode DNE in Plex, return return None From 60ed5d286c1414d3e2f4217f149e7e8bcac0e92c Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Tue, 14 Jun 2022 18:31:46 -0600 Subject: [PATCH 7/9] Log source image downloadin Log source image gathering in Debug level --- modules/Show.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/Show.py b/modules/Show.py index 8c4561937..722e87da4 100755 --- a/modules/Show.py +++ b/modules/Show.py @@ -670,6 +670,8 @@ def select_source_images(self, plex_interface: PlexInterface=None, # If URL was returned by either interface, download if image_url is not None: WebInterface.download_image(image_url, episode.source) + log.debug(f'Downloaded {episode.source.name} for {self} ' + f'from {source_interface.title()}') break # Query TMDb for the backdrop if one does not exist and is needed From 2409eee196607bea688983cd7b990c8693d08136 Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Tue, 14 Jun 2022 18:56:36 -0600 Subject: [PATCH 8/9] Allow for multiple translations per series Implements #179 --- modules/Show.py | 72 +++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/modules/Show.py b/modules/Show.py index 722e87da4..987b6be2e 100755 --- a/modules/Show.py +++ b/modules/Show.py @@ -255,13 +255,19 @@ def __parse_yaml(self): if (value := self._get('seasons', 'hide', type_=bool)) is not None: self.hide_seasons = value - if (self._is_specified('translation', 'language') - and (key := self._get('translation', 'key',type_=str)) is not None): - if key in ('title', 'abs_number'): - log.error(f'Cannot add translations under the key "{key}" in ' - f'series {self}') + if (value := self._get('translation')) is not None: + if isinstance(value, dict) and value.keys() == {'language', 'key'}: + # Single translation + self.title_languages = [value] + elif isinstance(value, list): + # List of translations + if all(isinstance(t, dict) and t.keys() == {'language', 'key'} + for t in value): + self.title_languages = value + else: + log.error(f'Invalid language translations in series {self}') else: - self.title_language = self._get('translation') + log.error(f'Invalid language translations in series {self}') # Construct EpisodeMap on seasons/episode ranges specification self.__episode_map = EpisodeMap( @@ -441,42 +447,44 @@ def add_translations(self, tmdb_interface: 'TMDbInterface') -> None: episode titles. """ - # If no title language was specified, or TMDb syncing isn't enabled,skip - if self.title_language == {} or not self.tmdb_sync: + # If no translations were specified, or TMDb syncing isn't enabled, skip + if len(self.title_languages) == 0 or not self.tmdb_sync: return None # Go through every episode and look for translations modified = False for _, episode in (pbar := tqdm(self.episodes.items(), **TQDM_KWARGS)): - # If the key already exists, skip this episode - if self.title_language['key'] in episode.extra_characteristics: - continue + # Get each translation for this series + for translation in self.title_languages: + # If the key already exists, skip this episode + if translation['key'] in episode.extra_characteristics: + continue - # Update progress bar - pbar.set_description(f'Checking {episode}') + # Update progress bar + pbar.set_description(f'Checking {episode}') - # Query TMDb for the title of this episode in the requested language - language_title = tmdb_interface.get_episode_title( - self.series_info, - episode.episode_info, - self.title_language['language'], - ) + # Query TMDb for the title of this episode in this language + language_title = tmdb_interface.get_episode_title( + self.series_info, + episode.episode_info, + translation['language'], + ) - # If episode wasn't found, or the original title was returned, skip! - if (language_title is None - or language_title == episode.episode_info.title.full_title): - continue + # If episode wasn't found, or original title was returned, skip + if (language_title is None + or language_title == episode.episode_info.title.full_title): + continue - # Adding translated title, log it - log.debug(f'Adding "{language_title}" to ' - f'"{self.title_language["key"]}" of {self}') + # Modify data file entry with new title + modified = True + self.file_interface.add_data_to_entry( + episode.episode_info, + **{translation['key']: language_title}, + ) - # Modify data file entry with new title - modified = True - self.file_interface.add_data_to_entry( - episode.episode_info, - **{self.title_language['key']: language_title}, - ) + # Adding translated title, log it + log.debug(f'Added "{language_title}" to ' + f'"{translation["key"]}" for {self}') # If any translations were added, re-read source if modified: From dc542b3024fc445bf893106bfaa7a55d226bfd0c Mon Sep 17 00:00:00 2001 From: Collin Heist Date: Tue, 14 Jun 2022 22:38:03 -0600 Subject: [PATCH 9/9] Fix top heavy splitting on special characters Fixes #187 --- modules/Show.py | 2 +- modules/Title.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/Show.py b/modules/Show.py index 987b6be2e..a965c63e3 100755 --- a/modules/Show.py +++ b/modules/Show.py @@ -96,7 +96,7 @@ def __init__(self, name: str, yaml_dict: dict, library_map: dict, self.unwatched_style = self.preferences.global_unwatched_style self.hide_seasons = False self.__episode_map = EpisodeMap() - self.title_language = {} + self.title_languages = {} self.extras = {} # Set object attributes based off YAML and update validity diff --git a/modules/Title.py b/modules/Title.py index 6a871d1a1..d0d78190e 100755 --- a/modules/Title.py +++ b/modules/Title.py @@ -137,12 +137,14 @@ def split(self, max_line_width: int, max_line_count: int, for _ in range(max_line_count+2-1): # Start splitting from the last line added top, bottom = all_lines.pop(), '' - while ((len(top) > max_line_width or len(bottom) in range(1, 6)) + while ((len(top) > max_line_width + or len(bottom) in range(1, 6)) and ' ' in top): # Look to split on special characters special_split = False for char in self.SPLIT_CHARACTERS: - if f'{char} ' in top[:max_line_width]: + # Split only if present after first third of next line + if f'{char} ' in top[max_line_width//2:max_line_width]: top, bottom_add = top.rsplit(f'{char} ', 1) top += char bottom = f'{bottom_add} {bottom}' @@ -172,12 +174,13 @@ def split(self, max_line_width: int, max_line_count: int, # For bottom heavy splitting, start on bottom and move text UP for _ in range(max_line_count+2-1): top, bottom = '', all_lines.pop() - while ((len(bottom) > max_line_width or len(top) in range(1, 6)) + while ((len(bottom) > max_line_width + or len(top) in range(1, 6)) and ' ' in bottom): # Look to split on special characters special_split = False for char in self.SPLIT_CHARACTERS: - if f'{char} ' in bottom[:min(max_line_width, len(bottom)//2)]: + if f'{char} ' in bottom[:min(max_line_width,len(bottom)//2)]: top_add, bottom = bottom.split(f'{char} ', 1) top = f'{top} {top_add}{char}' special_split = True