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)