diff --git a/modules/Debug.py b/modules/Debug.py index 9244f25d3..5b435bc8f 100755 --- a/modules/Debug.py +++ b/modules/Debug.py @@ -3,8 +3,11 @@ from tqdm import tqdm """TQDM bar format string""" -TQDM_BAR = ('{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} ' - '[{elapsed}, {rate_fmt}{postfix}]') +TQDM_KWARGS = { + 'bar_format': ('{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} ' + '[{elapsed}, {rate_fmt}{postfix}]'), + 'leave': False, +} class LogHandler(Handler): """Handler subclass to integrate logging messages with TQDM""" diff --git a/modules/Manager.py b/modules/Manager.py index 78ee77b9f..77e61aaba 100755 --- a/modules/Manager.py +++ b/modules/Manager.py @@ -1,7 +1,7 @@ from yaml import dump from tqdm import tqdm -from modules.Debug import log, TQDM_BAR +from modules.Debug import log, TQDM_KWARGS from modules.PlexInterface import PlexInterface import modules.preferences as global_preferences from modules.Show import Show @@ -93,7 +93,7 @@ def check_tmdb_for_translations(self) -> None: return None # For each show in the Manager, add translation - for show in (pbar := tqdm(self.shows, bar_format=TQDM_BAR)): + for show in (pbar := tqdm(self.shows, **TQDM_KWARGS)): pbar.set_description(f'Adding translations for ' f'"{show.series_info.short_name}"') show.add_translations(self.tmdb_interface) @@ -105,13 +105,14 @@ def read_show_source(self) -> None: for all Show and ShowArchives, and also looks for multipart episodes. """ - for show in tqdm(self.shows, desc='Reading source files', - bar_format=TQDM_BAR): + # Read source files for Show objects + for show in tqdm(self.shows, desc='Reading source files',**TQDM_KWARGS): show.read_source() show.find_multipart_episodes() + # Read source files for ShowSummary objects for archive in tqdm(self.archives, desc='Reading archive source files', - bar_format=TQDM_BAR): + **TQDM_KWARGS): archive.read_source() archive.find_multipart_episodes() @@ -128,7 +129,7 @@ def check_sonarr_for_new_episodes(self) -> None: # Go through each show in the Manager and query Sonarr for show in tqdm(self.shows + self.archives, desc='Querying Sonarr', - bar_format=TQDM_BAR): + **TQDM_KWARGS): show.query_sonarr(self.sonarr_interface) @@ -139,7 +140,7 @@ def create_missing_title_cards(self) -> None: """ # Go through every show in the Manager, create cards - for show in (pbar := tqdm(self.shows, bar_format=TQDM_BAR)): + for show in (pbar := tqdm(self.shows, **TQDM_KWARGS)): # Update progress bar pbar.set_description(f'Creating Title Cards for ' f'"{show.series_info.short_name}"') @@ -163,7 +164,7 @@ def update_plex(self) -> None: return None # Go through each show in the Manager, update Plex - for show in (pbar := tqdm(self.shows, bar_format=TQDM_BAR)): + for show in (pbar := tqdm(self.shows, **TQDM_KWARGS)): # Update progress bar pbar.set_description(f'Updating Plex for ' f'"{show.series_info.short_name}"') @@ -181,7 +182,7 @@ def update_archive(self) -> None: if not self.preferences.create_archive: return None - for show_archive in (pbar := tqdm(self.archives, bar_format=TQDM_BAR)): + for show_archive in (pbar := tqdm(self.archives, **TQDM_KWARGS)): # Update progress bar pbar.set_description(f'Updating archive for ' f'"{show_archive.series_info.short_name}"') @@ -202,7 +203,7 @@ def create_summaries(self) -> None: if not self.preferences.create_summaries: return None - for show_archive in (pbar := tqdm(self.archives, bar_format=TQDM_BAR)): + for show_archive in (pbar := tqdm(self.archives, **TQDM_KWARGS)): # Update progress bar pbar.set_description(f'Creating ShowSummary for "' f'{show_archive.series_info.short_name}"') diff --git a/modules/PlexInterface.py b/modules/PlexInterface.py index d0c128033..c7811634c 100755 --- a/modules/PlexInterface.py +++ b/modules/PlexInterface.py @@ -100,7 +100,7 @@ def __get_library(self, library_name: str) -> 'Library': try: return self.__server.library.section(library_name) except NotFound: - log.error(f'Library "{library}" was not found in Plex') + log.error(f'Library "{library_name}" was not found in Plex') return None diff --git a/modules/PreferenceParser.py b/modules/PreferenceParser.py index 2b1c9da30..ae3b39948 100755 --- a/modules/PreferenceParser.py +++ b/modules/PreferenceParser.py @@ -1,13 +1,15 @@ from pathlib import Path +from re import findall from tqdm import tqdm from yaml import safe_load -from modules.Debug import log, TQDM_BAR +from modules.Debug import log, TQDM_KWARGS from modules.ImageMagickInterface import ImageMagickInterface from modules.ImageMaker import ImageMaker from modules.Show import Show from modules.ShowSummary import ShowSummary +from modules.Template import Template from modules.TitleCard import TitleCard from modules.TMDbInterface import TMDbInterface from modules.YamlReader import YamlReader @@ -212,6 +214,46 @@ def __parse_yaml(self) -> None: self.imagemagick_container = value + def __apply_template(self, templates: dict, series_yaml: dict, + series_name: str) -> bool: + """ + Apply the correct Template object (if indicated) to the given series + YAML. This effectively "fill out" the indicated template, and updates + the series YAML directly. + + :param templates: Dictionary of Template objects to + potentially apply. + :param series_yaml: The YAML of the series to modify. + :param series_name: The name of the series being modified. + + :returns: True if the given series contained all the required template + variables for application, False if it did not. + """ + + # No templates defined, skip + if templates == {} or 'template' not in series_yaml: + return True + + # Get the specified template for this series + if isinstance((series_template := series_yaml['template']), str): + # Assume if only a string, then its the template name + series_template = {'name': series_template} + series_yaml['template'] = series_template + + # Warn and return if no template name given + if not (template_name := series_template.get('name', None)): + log.error(f'Missing template name for "{series_name}"') + return False + + # Warn and return if template name not mapped + if not (template := templates.get(template_name, None)): + log.error(f'Template "{template_name}" not defined') + return False + + # Apply using Template object + return template.apply_to_series(series_name, series_yaml) + + def read_file(self) -> None: """ Reads this associated preference file and store in `__yaml` attribute. @@ -243,41 +285,60 @@ def iterate_series_files(self) -> [Show]: """ # For each file in the cards list - for file in (pbar := tqdm(self.series_files, bar_format=TQDM_BAR)): + for file_ in (pbar := tqdm(self.series_files, **TQDM_KWARGS)): # Create Path object for this file - file_object = Path(file) + file = Path(file_) # Update progress bar for this file - pbar.set_description(f'Reading {file_object.name}') + pbar.set_description(f'Reading {file.name}') # If the file doesn't exist, error and skip - if not file_object.exists(): - log.error(f'Series file "{file_object.resolve()}" does not ' + if not file.exists(): + log.error(f'Series file "{file.resolve()}" does not ' f'exist') continue # Read file, parse yaml - if (file_yaml := self._read_file(file_object)) == {}: + if (file_yaml := self._read_file(file)) == {}: continue # Skip if there are no series to yield if file_yaml is None or 'series' not in file_yaml: - log.warning(f'Series file has no entries') + log.info(f'Series file has no entries') continue # Get library map for this file; error+skip missing library paths if (library_map := file_yaml.get('libraries', {})): if not all('path' in library_map[lib] for lib in library_map): - log.error(f'Libraries in series file "{file_object.resolve()}' - f'"are missing their "path" attributes.') + log.error(f'Libraries are missing required "path" in series' + f' file "{file.resolve()}"') continue # Get font map for this file font_map = file_yaml.get('fonts', {}) + # Get templates for this file + templates = {} + for name, template in file_yaml.get('templates', {}).items(): + # If not specified as dictionary, error and skip + if not isinstance(template, dict): + log.error(f'Invalid template specification for "{name}" in ' + f'series file "{file.resolve()}"') + continue + templates[name] = Template(name, template) + # Go through each series in this file - for show_name in tqdm(file_yaml['series'], leave=False, - desc='Creating Shows', bar_format=TQDM_BAR): + for show_name in tqdm(file_yaml['series'], desc='Creating Shows', + **TQDM_KWARGS): + # Apply template to series + valid = self.__apply_template( + templates, file_yaml['series'][show_name], show_name, + ) + + # Skip if series is not valid + if not valid: + continue + # Yield the Show object created from this entry yield Show( show_name, @@ -326,3 +387,4 @@ def get_season_folder(self, season_number: int) -> str: # Return non-zero-padded season name return f'Season {season_number}' + diff --git a/modules/Show.py b/modules/Show.py index 6544e5925..fe378bb3e 100755 --- a/modules/Show.py +++ b/modules/Show.py @@ -1,10 +1,9 @@ from pathlib import Path -from re import match from tqdm import tqdm from modules.DataFileInterface import DataFileInterface -from modules.Debug import log, TQDM_BAR +from modules.Debug import log, TQDM_KWARGS from modules.Episode import Episode from modules.Font import Font from modules.MultiEpisode import MultiEpisode @@ -63,11 +62,11 @@ def __init__(self, name: str, yaml_dict: dict, library_map: dict, return None # Year is given, parse and update year/full name of this show - if not match(r'^\d{4}$', str(year)): + if not isinstance(year, int) or year < 0: log.error(f'Year "{year}" of series "{name}" is invalid') self.valid = False return None - + # Setup default values that can be overwritten by YAML self.series_info = SeriesInfo(name, year) self.media_directory = None @@ -81,7 +80,7 @@ def __init__(self, name: str, yaml_dict: dict, library_map: dict, self.tmdb_sync = True self.hide_seasons = False self.__episode_range = {} - self.__season_map = {n: f'Season {n}' for n in range(1, 1000)} + self.__season_map = {n: f'Season {n}' for n in range(1, 100)} self.__season_map[0] = 'Specials' self.title_language = {} @@ -141,19 +140,18 @@ def __parse_yaml(self): if (library := self['library']): # If the given library isn't in libary map, invalid - if library not in self.__library_map: + if not (this_library := self.__library_map.get(library, None)): log.error(f'Library "{library}" of series {self} is not found ' f'in libraries list') self.valid = False else: # Valid library, update library and media directory - self.library_name = self.__library_map.get('plex_name', library) - self.library = Path(self.__library_map[library]['path']) + self.library_name = this_library.get('plex_name', library) + self.library = Path(this_library['path']) self.media_directory = self.library / self.series_info.full_name # If card type was specified for this library, set that - if 'card_type' in self.__library_map[library]: - card_type = self.__library_map[library]['card_type'] + if (card_type := this_library.get('card_type', None)): if card_type not in TitleCard.CARD_TYPES: log.error(f'Unknown card type "{card_type}" of series ' f'{self}') @@ -400,8 +398,7 @@ def add_translations(self, tmdb_interface: 'TMDbInterface') -> None: # Go through every episode and look for translations modified = False - for _, episode in (pbar := tqdm(self.episodes.items(), leave=False, - bar_format=TQDM_BAR)): + for _, episode in (pbar := tqdm(self.episodes.items(), **TQDM_KWARGS)): # Update progress bar pbar.set_description(f'Checking {episode}') @@ -451,8 +448,7 @@ def create_missing_title_cards(self, return False # Go through each episode for this show - for _, episode in (pbar := tqdm(self.episodes.items(), leave=False, - bar_format=TQDM_BAR)): + for _, episode in (pbar := tqdm(self.episodes.items(), **TQDM_KWARGS)): # Update progress bar pbar.set_description(f'Creating {episode}') diff --git a/modules/SonarrInterface.py b/modules/SonarrInterface.py index 32deb447a..3f0520193 100755 --- a/modules/SonarrInterface.py +++ b/modules/SonarrInterface.py @@ -183,7 +183,7 @@ def __get_all_episode_info(self, series_id: int) -> [EpisodeInfo]: # Go through each episode and get its season/episode number, and title for episode in all_episodes: # Unaired episodes (such as specials) won't have airDateUtc key - if 'airDateUtc' in episode: + if 'airDateUtc' in episode and not episode['hasFile']: # Verify this episode has already aired, skip if not air_datetime = datetime.strptime( episode['airDateUtc'], diff --git a/modules/Template.py b/modules/Template.py new file mode 100755 index 000000000..45d3014c7 --- /dev/null +++ b/modules/Template.py @@ -0,0 +1,154 @@ +from re import findall + +from modules.Debug import log + +class Template: + """ + This class describes a template. A Template is a fallback YAML object that + can be "filled in" with values, or just outright contain them. Variable + data is encoded in the form <<{key}>>. When applied to some series YAML + dictionary, the template'd YAML is applied to the series, unless both have + instances of the data, in which the series data takes priority. + """ + + def __init__(self, name: str, template: dict) -> None: + """ + Construct a new Template object with the given name, and with the given + template dictionary. Keys of the form <<{key}>> are search for through + this template. + + :param name: The template name/identifier. For logging only. + :param template: The template YAML to implement. + """ + + self.name = name + self.__template = template + self.__keys = self.__identify_template_keys(self.__template, set()) + + + def __repr__(self) -> str: + """Returns a unambiguous string representation of the object.""" + + return f'