diff --git a/CHANGES.md b/CHANGES.md index a94b40b2..e2c9af86 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ * Change remove redundant py2 import futures * Change add jobs to centralise scheduler activities * Change refactor scene_exceptions +* Add to config/media-process/File Handling, "Rename TBA" and "Rename any" * Add config to change media process log message if there is no media to process * Change view-show text "invalid timeformat" to "time unknown" diff --git a/gui/slick/interfaces/default/config_postProcessing.tmpl b/gui/slick/interfaces/default/config_postProcessing.tmpl index a3a4a191..fd207464 100644 --- a/gui/slick/interfaces/default/config_postProcessing.tmpl +++ b/gui/slick/interfaces/default/config_postProcessing.tmpl @@ -242,10 +242,30 @@
+
+ +
+ +
+ +
+
diff --git a/sickgear/__init__.py b/sickgear/__init__.py index de2f55ea..a5800cb7 100644 --- a/sickgear/__init__.py +++ b/sickgear/__init__.py @@ -284,6 +284,8 @@ ADD_SHOWS_METALANG = 'en' CREATE_MISSING_SHOW_DIRS = False SHOW_DIRS_WITH_DOTS = False RENAME_EPISODES = False +RENAME_TBA_EPISODES = True +RENAME_NAME_CHANGED_EPISODES = False AIRDATE_EPISODES = False PROCESS_AUTOMATICALLY = False KEEP_PROCESSED_DIR = False @@ -717,8 +719,9 @@ def init_stage_1(console_logging): global TV_DOWNLOAD_DIR, PROCESS_METHOD, PROCESS_AUTOMATICALLY, MEDIAPROCESS_INTERVAL, \ POSTPONE_IF_SYNC_FILES, PROCESS_POSITIVE_LOG, EXTRA_SCRIPTS, SG_EXTRA_SCRIPTS, \ DEFAULT_MEDIAPROCESS_INTERVAL, MIN_MEDIAPROCESS_INTERVAL, \ - UNPACK, SKIP_REMOVED_FILES, MOVE_ASSOCIATED_FILES, NFO_RENAME, RENAME_EPISODES, AIRDATE_EPISODES, \ - USE_FAILED_DOWNLOADS, DELETE_FAILED + UNPACK, SKIP_REMOVED_FILES, MOVE_ASSOCIATED_FILES, NFO_RENAME, \ + RENAME_EPISODES, RENAME_TBA_EPISODES, RENAME_NAME_CHANGED_EPISODES, \ + AIRDATE_EPISODES, USE_FAILED_DOWNLOADS, DELETE_FAILED # Media Process/Episode Naming global NAMING_PATTERN, NAMING_MULTI_EP, NAMING_STRIP_YEAR, NAMING_CUSTOM_ABD, NAMING_ABD_PATTERN, \ NAMING_CUSTOM_SPORTS, NAMING_SPORTS_PATTERN, \ @@ -1013,6 +1016,8 @@ def init_stage_1(console_logging): PROCESS_AUTOMATICALLY = bool(check_setting_int(CFG, 'General', 'process_automatically', 0)) UNPACK = bool(check_setting_int(CFG, 'General', 'unpack', 0)) RENAME_EPISODES = bool(check_setting_int(CFG, 'General', 'rename_episodes', 1)) + RENAME_TBA_EPISODES = bool(check_setting_int(CFG, 'General', 'rename_tba_episodes', 1)) + RENAME_NAME_CHANGED_EPISODES = bool(check_setting_int(CFG, 'General', 'rename_name_changed_episodes', 0)) AIRDATE_EPISODES = bool(check_setting_int(CFG, 'General', 'airdate_episodes', 0)) KEEP_PROCESSED_DIR = bool(check_setting_int(CFG, 'General', 'keep_processed_dir', 1)) PROCESS_METHOD = check_setting_str(CFG, 'General', 'process_method', 'copy' if KEEP_PROCESSED_DIR else 'move') @@ -1994,6 +1999,8 @@ def save_config(): new_config['General']['process_automatically'] = int(PROCESS_AUTOMATICALLY) new_config['General']['unpack'] = int(UNPACK) new_config['General']['rename_episodes'] = int(RENAME_EPISODES) + new_config['General']['rename_tba_episodes'] = int(RENAME_TBA_EPISODES) + new_config['General']['rename_name_changed_episodes'] = int(RENAME_NAME_CHANGED_EPISODES) new_config['General']['airdate_episodes'] = int(AIRDATE_EPISODES) new_config['General']['create_missing_show_dirs'] = int(CREATE_MISSING_SHOW_DIRS) new_config['General']['show_dirs_with_dots'] = int(SHOW_DIRS_WITH_DOTS) diff --git a/sickgear/helpers.py b/sickgear/helpers.py index 4d610df8..487db401 100644 --- a/sickgear/helpers.py +++ b/sickgear/helpers.py @@ -422,7 +422,7 @@ def move_and_symlink_file(src_file, dest_file): copy_file(src_file, dest_file) -def rename_ep_file(cur_path, new_path, old_path_length=0): +def rename_ep_file(cur_path, new_path, old_path_length=0, use_rename=False): """ Creates all folders needed to move a file to its new location, renames it, then cleans up any folders left that are now empty. @@ -433,6 +433,7 @@ def rename_ep_file(cur_path, new_path, old_path_length=0): :type new_path: AnyStr :param old_path_length: The length of media file path (old name) WITHOUT THE EXTENSION :type old_path_length: int or long + :param use_rename: use rename instead of shutil.move :return: success :rtype: bool """ @@ -466,8 +467,11 @@ def rename_ep_file(cur_path, new_path, old_path_length=0): # move the file try: logger.log(f'Renaming file from {cur_path} to {new_path}') - shutil.move(cur_path, new_path) - except (OSError, IOError) as e: + if use_rename: + os.rename(cur_path, new_path) + else: + shutil.move(cur_path, new_path) + except (OSError, IOError, IsADirectoryError, NotADirectoryError, FileExistsError) as e: logger.error(f'Failed renaming {cur_path} to {new_path}: {ex(e)}') return False diff --git a/sickgear/notifiers/__init__.py b/sickgear/notifiers/__init__.py index b35ae421..0882a00c 100644 --- a/sickgear/notifiers/__init__.py +++ b/sickgear/notifiers/__init__.py @@ -132,7 +132,7 @@ def notify_git_update(new_version=''): n.notify_git_update(new_version) -def notify_update_library(ep_obj, flush_q=False): +def notify_update_library(ep_obj, flush_q=False, include_online=True): if not flush_q or sickgear.QUEUE_UPDATE_LIBRARY: @@ -160,7 +160,8 @@ def notify_update_library(ep_obj, flush_q=False): elif not flush_q: - n.update_library(show_obj=ep_obj.show_obj, show_name=ep_obj.show_obj.name, ep_obj=ep_obj) + n.update_library(show_obj=ep_obj.show_obj, show_name=ep_obj.show_obj.name, ep_obj=ep_obj, + include_online=include_online) if flush_q: sickgear.QUEUE_UPDATE_LIBRARY = [] diff --git a/sickgear/notifiers/generic.py b/sickgear/notifiers/generic.py index a9f14ff8..d546b87c 100644 --- a/sickgear/notifiers/generic.py +++ b/sickgear/notifiers/generic.py @@ -91,9 +91,13 @@ class BaseNotifier(object): def notify_git_update(self, *args, **kwargs): pass - def update_library(self, **kwargs): + def update_library(self, include_online=True, **kwargs): """ note: nmj_notifier fires its library update when the notify_download is issued (inside notifiers) + + :param include_online: Set False if to exclude derived notifiers from library updates. + For example, Trakt doesn't need to be updated when renaming the episode name part of a filename, + therefore, this is set False for that specific notification use case update. """ pass diff --git a/sickgear/notifiers/trakt.py b/sickgear/notifiers/trakt.py index cb24c4ff..9bdd024f 100644 --- a/sickgear/notifiers/trakt.py +++ b/sickgear/notifiers/trakt.py @@ -41,9 +41,10 @@ class TraktNotifier(BaseNotifier): return True return False - def update_library(self, ep_obj=None, **kwargs): + def update_library(self, ep_obj=None, include_online=True, **kwargs): - self._update_collection(ep_obj) + if include_online: + self._update_collection(ep_obj) def _update_collection(self, ep_obj): """ diff --git a/sickgear/tv.py b/sickgear/tv.py index 6aa7c28a..a74a9351 100644 --- a/sickgear/tv.py +++ b/sickgear/tv.py @@ -78,6 +78,10 @@ if coreid_warnings: tz_p = du_parser() invalid_date_limit = datetime.date(1900, 1, 1) +tba_tvinfo_name = re.compile(r'^(episode \d+|tb[ad])$', flags=re.I) +tba_file_name = re.compile(r'\b(episode.\d+|tb[ad])\b', flags=re.I) +pattern_ep_name = re.compile(r'%E[._]?N', flags=re.I) + # status codes for switching tv show source TVSWITCH_DUPLICATE_SHOW = 0 TVSWITCH_ID_CONFLICT = 1 @@ -4301,6 +4305,8 @@ class TVEpisode(TVEpisodeBase): if self._name != self.dict_prevent_nonetype(ep_info, 'episodename'): switch_list.append(self.show_obj.switch_ep_change_sql(old_tvid, old_prodid, episode, season, TVSWITCH_EP_RENAMED)) + + old_name = self._name or '' self.name = self.dict_prevent_nonetype(ep_info, 'episodename') self.season = season self.episode = episode @@ -4462,6 +4468,22 @@ class TVEpisode(TVEpisodeBase): self.status = Quality.status_from_name_or_file(self._location, anime=self._show_obj.is_anime) logger.debug('%s%s' % (msg, statusStrings[self._status])) + if sickgear.RENAME_EPISODES and self.with_ep_name() \ + and os.path.splitext(ep_filename := os.path.basename(self._location or ''))[0] != \ + os.path.basename(self.proper_path()) \ + and (sickgear.RENAME_NAME_CHANGED_EPISODES + or (sickgear.RENAME_TBA_EPISODES + and (bool(tba_tvinfo_name.search(old_name)) + or (not bool(tba_tvinfo_name.search(self._name or '')) + and bool(tba_file_name.search(ep_filename or '')))) + )) \ + and os.path.isfile(self._location): + # noinspection PySimplifyBooleanCheck + if re_res := self.rename(): + notifiers.notify_update_library(self, include_online=False) + elif False == re_res: + logger.debug('Failed to rename files to TV info episode name') + # shouldn't get here probably else: msg = '(2) Status changes from %s to ' % statusStrings[self._status] @@ -5121,17 +5143,8 @@ class TVEpisode(TVEpisodeBase): """ Just the folder name of the episode """ - if None is pattern: - # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep - if self._show_obj.air_by_date and sickgear.NAMING_CUSTOM_ABD and not self.related_ep_obj: - pattern = sickgear.NAMING_ABD_PATTERN - elif self._show_obj.sports and sickgear.NAMING_CUSTOM_SPORTS and not self.related_ep_obj: - pattern = sickgear.NAMING_SPORTS_PATTERN - elif self._show_obj.anime and sickgear.NAMING_CUSTOM_ANIME: - pattern = sickgear.NAMING_ANIME_PATTERN - else: - pattern = sickgear.NAMING_PATTERN + pattern = self.naming_pattern() # split off the dirs only, if they exist name_groups = re.split(r'[\\/]', pattern) @@ -5145,24 +5158,35 @@ class TVEpisode(TVEpisodeBase): """ Just the filename of the episode, formatted based on the naming settings """ - - if None is pattern: - # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep - if self._show_obj.air_by_date and sickgear.NAMING_CUSTOM_ABD and not self.related_ep_obj: - pattern = sickgear.NAMING_ABD_PATTERN - elif self._show_obj.sports and sickgear.NAMING_CUSTOM_SPORTS and not self.related_ep_obj: - pattern = sickgear.NAMING_SPORTS_PATTERN - elif self._show_obj.anime and sickgear.NAMING_CUSTOM_ANIME: - pattern = sickgear.NAMING_ANIME_PATTERN - else: - pattern = sickgear.NAMING_PATTERN - # split off the dirs only, if they exist - name_groups = re.split(r'[\\/]', pattern) + name_groups = re.split(r'[\\/]', pattern or self.naming_pattern()) return self._format_pattern(name_groups[-1], multi, anime_type) + def with_ep_name(self): + # type: (...) -> bool + """ + returns if the episode naming contain the episode name + """ + return bool(pattern_ep_name.search(self.naming_pattern())) + + def naming_pattern(self): + # type: (...) -> AnyStr + """ + return a naming pattern for this show + """ + # we only use ABD if it's enabled, this is an ABD show, AND this is not a multi-ep + if self._show_obj.air_by_date and sickgear.NAMING_CUSTOM_ABD and not self.related_ep_obj: + return sickgear.NAMING_ABD_PATTERN + if self._show_obj.sports and sickgear.NAMING_CUSTOM_SPORTS and not self.related_ep_obj: + return sickgear.NAMING_SPORTS_PATTERN + if self._show_obj.anime and sickgear.NAMING_CUSTOM_ANIME: + return sickgear.NAMING_ANIME_PATTERN + + return sickgear.NAMING_PATTERN + def rename(self): + # type: (...) -> Optional[bool] """ Renames an episode file and all related files to the location and filename as specified in the naming settings. @@ -5202,25 +5226,31 @@ class TVEpisode(TVEpisodeBase): logger.debug('Files associated to %s: %s' % (self.location, related_files)) # move the ep file - result = helpers.rename_ep_file(self.location, absolute_proper_path, absolute_current_path_no_ext_length) + result = helpers.rename_ep_file(self.location, absolute_proper_path, absolute_current_path_no_ext_length, + use_rename=True) + any_renamed = all_renamed = result # move related files for cur_related_file in related_files: renamed = helpers.rename_ep_file(cur_related_file, absolute_proper_path, - absolute_current_path_no_ext_length) + absolute_current_path_no_ext_length, use_rename=True) + any_renamed |= renamed + all_renamed &= renamed if not renamed: logger.error('%s: Unable to rename file %s' % (self._epid, cur_related_file)) for cur_related_sub in related_subs: absolute_proper_subs_path = os.path.join(sickgear.SUBTITLES_DIR, self.formatted_filename()) renamed = helpers.rename_ep_file(cur_related_sub, absolute_proper_subs_path, - absolute_current_path_no_ext_length) + absolute_current_path_no_ext_length, use_rename=True) + any_renamed |= renamed + all_renamed &= renamed if not renamed: logger.error('%s: Unable to rename file %s' % (self._epid, cur_related_sub)) # save the ep - with self.lock: - if result: + if any_renamed: + with self.lock: self.location = absolute_proper_path + file_ext for ep_obj in self.related_ep_obj: ep_obj.location = absolute_proper_path + file_ext @@ -5233,14 +5263,16 @@ class TVEpisode(TVEpisodeBase): sql_l = [] with self.lock: for ep_obj in [self] + self.related_ep_obj: - result = ep_obj.get_sql() - if None is not result: - sql_l.append(result) + ep_sql = ep_obj.get_sql() + if None is not ep_sql: + sql_l.append(ep_sql) if 0 < len(sql_l): my_db = db.DBConnection() my_db.mass_action(sql_l) + return all_renamed + def airdate_modify_stamp(self): """ Make modify date and time of a file reflect the show air date and time. diff --git a/sickgear/webserve.py b/sickgear/webserve.py index c75944f1..05a1b17c 100644 --- a/sickgear/webserve.py +++ b/sickgear/webserve.py @@ -8498,20 +8498,20 @@ class ConfigMediaProcess(Config): t.submenu = self.config_menu('Processing') return t.respond() - def save_post_processing(self, tv_download_dir=None, process_automatically=None, mediaprocess_interval=None, - unpack=None, keep_processed_dir=None, process_method=None, - extra_scripts='', sg_extra_scripts='', - rename_episodes=None, airdate_episodes=None, - move_associated_files=None, postpone_if_sync_files=None, process_positive_log=None, - naming_custom_abd=None, naming_custom_sports=None, naming_custom_anime=None, - naming_strip_year=None, use_failed_downloads=None, delete_failed=None, - skip_removed_files=None, nfo_rename=None, - xbmc_data=None, xbmc_12plus_data=None, mediabrowser_data=None, sony_ps3_data=None, - wdtv_data=None, tivo_data=None, mede8er_data=None, kodi_data=None, - naming_pattern=None, naming_multi_ep=None, - naming_anime=None, naming_anime_pattern=None, naming_anime_multi_ep=None, - naming_abd_pattern=None, naming_sports_pattern=None, - **kwargs): # kwargs picks up deprecated vars sent from legacy UIs + def save_post_processing( + self, tv_download_dir=None, process_method=None, process_automatically=None, mediaprocess_interval=None, + postpone_if_sync_files=None, process_positive_log=None, extra_scripts='', sg_extra_scripts='', + unpack=None, skip_removed_files=None, move_associated_files=None, nfo_rename=None, + rename_episodes=None, rename_tba_episodes=None, rename_name_changed_episodes=None, + airdate_episodes=None, use_failed_downloads=None, delete_failed=None, + naming_pattern=None, naming_multi_ep=None, naming_strip_year=None, + naming_custom_abd=None, naming_abd_pattern=None, + naming_custom_sports=None, naming_sports_pattern=None, + naming_custom_anime=None, naming_anime_pattern=None,naming_anime_multi_ep=None, naming_anime=None, + kodi_data=None, mede8er_data=None, xbmc_data=None, mediabrowser_data=None, + sony_ps3_data=None, tivo_data=None, wdtv_data=None, xbmc_12plus_data=None, + keep_processed_dir=None, + **kwargs): # kwargs picks up deprecated vars sent from legacy UIs results = [] @@ -8536,6 +8536,8 @@ class ConfigMediaProcess(Config): sickgear.EXTRA_SCRIPTS = [x.strip() for x in extra_scripts.split('|') if x.strip()] sickgear.SG_EXTRA_SCRIPTS = [x.strip() for x in sg_extra_scripts.split('|') if x.strip()] sickgear.RENAME_EPISODES = config.checkbox_to_value(rename_episodes) + sickgear.RENAME_TBA_EPISODES = config.checkbox_to_value(rename_tba_episodes) + sickgear.RENAME_NAME_CHANGED_EPISODES = config.checkbox_to_value(rename_name_changed_episodes) sickgear.AIRDATE_EPISODES = config.checkbox_to_value(airdate_episodes) sickgear.MOVE_ASSOCIATED_FILES = config.checkbox_to_value(move_associated_files) sickgear.POSTPONE_IF_SYNC_FILES = config.checkbox_to_value(postpone_if_sync_files)