diff --git a/CHANGELOG.md b/CHANGELOG.md index 0548479aa2..b04afe6dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - Fixed adding anime release group when adding show ([#5749](https://github.com/pymedusa/Medusa/pull/5749)) - Fixed Pushover debug log causing BraceException ([#5759](https://github.com/pymedusa/Medusa/pull/5759)) - Fixed torrent method Downloadstation not selected after restart ([#5761](https://github.com/pymedusa/Medusa/pull/5761)) +- Fixed changing show location, should now also utilise the option 'CREATE_MISSING_SHOW_DIRS' ([#5795](https://github.com/pymedusa/Medusa/pull/5795)) ----- diff --git a/medusa/clients/torrent/utorrent_client.py b/medusa/clients/torrent/utorrent_client.py index eecaff5103..7b84f525a5 100644 --- a/medusa/clients/torrent/utorrent_client.py +++ b/medusa/clients/torrent/utorrent_client.py @@ -26,7 +26,7 @@ def get_torrent_subfolder(result): root_dirs = app.ROOT_DIRS if root_dirs: root_location = root_dirs[int(root_dirs[0]) + 1] - torrent_path = result.series.raw_location + torrent_path = result.series.location if root_dirs and root_location != torrent_path: # Subfolder is under root, but possibly not directly under diff --git a/medusa/metadata/generic.py b/medusa/metadata/generic.py index afdb26c618..6ca1a2c0bf 100644 --- a/medusa/metadata/generic.py +++ b/medusa/metadata/generic.py @@ -165,19 +165,19 @@ def _has_season_all_banner(self, show_obj): return self._check_exists(self.get_season_all_banner_path(show_obj)) def get_show_file_path(self, show_obj): - return os.path.join(show_obj.location, self._show_metadata_filename) + return os.path.join(show_obj.validate_location, self._show_metadata_filename) def get_episode_file_path(self, ep_obj): - return replace_extension(ep_obj.location, self._ep_nfo_extension) + return replace_extension(ep_obj.validate_location, self._ep_nfo_extension) def get_fanart_path(self, show_obj): - return os.path.join(show_obj.location, self.fanart_name) + return os.path.join(show_obj.validate_location, self.fanart_name) def get_poster_path(self, show_obj): - return os.path.join(show_obj.location, self.poster_name) + return os.path.join(show_obj.validate_location, self.poster_name) def get_banner_path(self, show_obj): - return os.path.join(show_obj.location, self.banner_name) + return os.path.join(show_obj.validate_location, self.banner_name) def get_image_path(self, show_obj, image_type): """Based on the image_type (banner, poster, fanart) call the correct method, and return the path.""" @@ -224,7 +224,7 @@ def get_season_poster_path(show_obj, season): else: season_poster_filename = u'season' + str(season).zfill(2) - return os.path.join(show_obj.location, season_poster_filename + u'-poster.jpg') + return os.path.join(show_obj.validate_location, season_poster_filename + u'-poster.jpg') @staticmethod def get_season_banner_path(show_obj, season): @@ -241,13 +241,13 @@ def get_season_banner_path(show_obj, season): else: season_banner_filename = u'season' + str(season).zfill(2) - return os.path.join(show_obj.location, season_banner_filename + u'-banner.jpg') + return os.path.join(show_obj.validate_location, season_banner_filename + u'-banner.jpg') def get_season_all_poster_path(self, show_obj): - return os.path.join(show_obj.location, self.season_all_poster_name) + return os.path.join(show_obj.validate_location, self.season_all_poster_name) def get_season_all_banner_path(self, show_obj): - return os.path.join(show_obj.location, self.season_all_banner_name) + return os.path.join(show_obj.validate_location, self.season_all_banner_name) # pylint: disable=unused-argument,no-self-use def _show_data(self, show_obj): diff --git a/medusa/metadata/kodi.py b/medusa/metadata/kodi.py index 9605643636..fd920f229a 100644 --- a/medusa/metadata/kodi.py +++ b/medusa/metadata/kodi.py @@ -104,7 +104,7 @@ def get_season_poster_path(show_obj, season): else: season_poster_filename = 'season' + text_type(season).zfill(2) - return os.path.join(show_obj.location, season_poster_filename + '.tbn') + return os.path.join(show_obj.validate_location, season_poster_filename + '.tbn') # present a standard "interface" from the module diff --git a/medusa/metadata/media_browser.py b/medusa/metadata/media_browser.py index c6097a6454..8df7f905fc 100644 --- a/medusa/metadata/media_browser.py +++ b/medusa/metadata/media_browser.py @@ -138,8 +138,8 @@ def get_season_poster_path(show_obj, season): If no season folder exists, None is returned """ - dir_list = [x for x in os.listdir(show_obj.location) if - os.path.isdir(os.path.join(show_obj.location, x))] + dir_list = [x for x in os.listdir(show_obj.validate_location) if + os.path.isdir(os.path.join(show_obj.validate_location, x))] season_dir_regex = r'^Season\s+(\d+)$' @@ -170,7 +170,7 @@ def get_season_poster_path(show_obj, season): log.debug(u'Using {path}/folder.jpg as season directory for season {number}', {u'path': season_dir, u'number': season}) - return os.path.join(show_obj.location, season_dir, u'folder.jpg') + return os.path.join(show_obj.validate_location, season_dir, u'folder.jpg') @staticmethod def get_season_banner_path(show_obj, season): @@ -180,8 +180,8 @@ def get_season_banner_path(show_obj, season): If no season folder exists, None is returned """ - dir_list = [x for x in os.listdir(show_obj.location) if - os.path.isdir(os.path.join(show_obj.location, x))] + dir_list = [x for x in os.listdir(show_obj.validate_location) if + os.path.isdir(os.path.join(show_obj.validate_location, x))] season_dir_regex = r'^Season\s+(\d+)$' @@ -212,7 +212,7 @@ def get_season_banner_path(show_obj, season): log.debug(u'Using {path}/banner.jpg as season directory for season {number}', {u'path': season_dir, u'number': season}) - return os.path.join(show_obj.location, season_dir, u'banner.jpg') + return os.path.join(show_obj.validate_location, season_dir, u'banner.jpg') def _show_data(self, show_obj): """ diff --git a/medusa/metadata/wdtv.py b/medusa/metadata/wdtv.py index 3f3c03fa90..c17feac597 100644 --- a/medusa/metadata/wdtv.py +++ b/medusa/metadata/wdtv.py @@ -130,8 +130,8 @@ def get_season_poster_path(show_obj, season): If no season folder exists, None is returned """ - dir_list = [x for x in os.listdir(show_obj.location) if - os.path.isdir(os.path.join(show_obj.location, x))] + dir_list = [x for x in os.listdir(show_obj.validate_location) if + os.path.isdir(os.path.join(show_obj.validate_location, x))] season_dir_regex = r'^Season\s+(\d+)$' @@ -159,7 +159,7 @@ def get_season_poster_path(show_obj, season): log.debug('Using {location}/folder.jpg as season dir for season {number}', {'location': season_dir, 'number': season}) - return os.path.join(show_obj.location, season_dir, 'folder.jpg') + return os.path.join(show_obj.validate_location, season_dir, 'folder.jpg') def _ep_data(self, ep_obj): """ diff --git a/medusa/post_processor.py b/medusa/post_processor.py index c1d4d9cb56..88395ef633 100644 --- a/medusa/post_processor.py +++ b/medusa/post_processor.py @@ -1203,11 +1203,11 @@ def process(self): # find the destination folder try: proper_path = ep_obj.proper_path() - proper_absolute_path = os.path.join(ep_obj.series.location, proper_path) + proper_absolute_path = os.path.join(ep_obj.series.validate_location, proper_path) dest_path = os.path.dirname(proper_absolute_path) except ShowDirectoryNotFoundException: raise EpisodePostProcessingFailedException(u"Unable to post-process an episode if the show dir '{0}' " - u"doesn't exist, quitting".format(ep_obj.series.raw_location)) + u"doesn't exist, quitting".format(ep_obj.series.location)) self.log(u'Destination folder for this episode: {0}'.format(dest_path), logger.DEBUG) diff --git a/medusa/server/api/v1/core.py b/medusa/server/api/v1/core.py index f40aed33aa..1474e44c9e 100644 --- a/medusa/server/api/v1/core.py +++ b/medusa/server/api/v1/core.py @@ -734,7 +734,7 @@ def run(self): # absolute vs relative vs broken show_path = None try: - show_path = show_obj.location + show_path = show_obj.validate_location except ShowDirectoryNotFoundException: pass @@ -1903,7 +1903,7 @@ def run(self): show_dict['quality_details'] = {'initial': any_qualities, 'archive': best_qualities} try: - show_dict['location'] = show_obj.location + show_dict['location'] = show_obj.validate_location except ShowDirectoryNotFoundException: show_dict['location'] = '' diff --git a/medusa/server/api/v2/series.py b/medusa/server/api/v2/series.py index f34da0f60c..ed5a6b44d7 100644 --- a/medusa/server/api/v2/series.py +++ b/medusa/server/api/v2/series.py @@ -130,7 +130,7 @@ def http_patch(self, series_slug, path_param=None): 'config.scene': BooleanField(series, 'scene'), 'config.sports': BooleanField(series, 'sports'), 'config.paused': BooleanField(series, 'paused'), - 'config.location': StringField(series, '_location'), + 'config.location': StringField(series, 'location'), 'config.airByDate': BooleanField(series, 'air_by_date'), 'config.subtitlesEnabled': BooleanField(series, 'subtitles'), 'config.release.requiredWords': ListField(series, 'release_required_words'), diff --git a/medusa/server/web/home/handler.py b/medusa/server/web/home/handler.py index de9f19e0f9..6f15097943 100644 --- a/medusa/server/web/home/handler.py +++ b/medusa/server/web/home/handler.py @@ -823,7 +823,7 @@ def displayShow(self, indexername=None, seriesid=None, ): }] try: - show_loc = (series_obj.location, True) + show_loc = (series_obj.validate_location, True) except ShowDirectoryNotFoundException: show_loc = (series_obj._location, False) # pylint: disable=protected-access @@ -1146,7 +1146,7 @@ def snatchSelection(self, indexername, seriesid, season=None, episode=None, manu }] try: - show_loc = (series_obj.location, True) + show_loc = (series_obj.validate_location, True) except ShowDirectoryNotFoundException: show_loc = (series_obj._location, False) # pylint: disable=protected-access @@ -2016,7 +2016,7 @@ def testRename(self, indexername=None, seriesid=None): return self._genericMessage('Error', 'Show not in show list') try: - series_obj.location # @UnusedVariable + series_obj.validate_location # @UnusedVariable except ShowDirectoryNotFoundException: return self._genericMessage('Error', 'Can\'t rename episodes when the show dir is missing.') @@ -2051,7 +2051,7 @@ def doRename(self, indexername=None, seriesid=None, eps=None): return self._genericMessage('Error', error_message) try: - series_obj.location # @UnusedVariable + series_obj.validate_location # @UnusedVariable except ShowDirectoryNotFoundException: return self._genericMessage('Error', 'Can\'t rename episodes when the show dir is missing.') diff --git a/medusa/show_queue.py b/medusa/show_queue.py index c68418d793..6589ba5538 100644 --- a/medusa/show_queue.py +++ b/medusa/show_queue.py @@ -716,7 +716,7 @@ def run(self): ) try: - self.show.location + self.show.validate_location except ShowDirectoryNotFoundException: log.warning( "Can't perform rename on {series_name} when the show dir is missing.", diff --git a/medusa/tv/episode.py b/medusa/tv/episode.py index a9ec1b636d..62cb5f68ff 100644 --- a/medusa/tv/episode.py +++ b/medusa/tv/episode.py @@ -865,7 +865,7 @@ def load_from_indexer(self, season=None, episode=None, tvapi=None, cached_season '{id}: {series} episode statuses unchanged. Location is missing: {location}', { 'id': self.series.series_id, 'series': self.series.name, - 'location': self.series.raw_location, + 'location': self.series.location, } ) return diff --git a/medusa/tv/series.py b/medusa/tv/series.py index 6daa6ca70e..1beddb1d69 100644 --- a/medusa/tv/series.py +++ b/medusa/tv/series.py @@ -55,6 +55,7 @@ ) from medusa.helper.exceptions import ( AnidbAdbaConnectionException, + CantRefreshShowException, CantRemoveShowException, EpisodeDeletedException, EpisodeNotFoundException, @@ -365,43 +366,77 @@ def network_logo_name(self): return self.network.replace(u'\u00C9', 'e').replace(u'\u00E9', 'e').replace(' ', '-').lower() @property - def raw_location(self): - """Get the raw show location, unvalidated.""" - return self._location - - @property - def location(self): - """Get the show location.""" - # no directory check needed if missing - # show directories are created during post-processing + def validate_location(self): + """Legacy call to location with a validation when ADD_SHOWS_WO_DIR is set.""" if app.CREATE_MISSING_SHOW_DIRS or self.is_location_valid(): return self._location raise ShowDirectoryNotFoundException(u'Show folder does not exist.') @property - def indexer_name(self): - """Return the indexer name identifier. Example: tvdb.""" - return indexerConfig[self.indexer].get('identifier') - - @property - def indexer_slug(self): - """Return the slug name of the series. Example: tvdb1234.""" - return indexer_id_to_slug(self.indexer, self.series_id) + def location(self): + """Get the show location.""" + return self._location @location.setter def location(self, value): + old_location = os.path.normpath(self._location) + new_location = os.path.normpath(value) + log.debug( u'{indexer} {id}: Setting location: {location}', { 'indexer': indexerApi(self.indexer).name, 'id': self.series_id, - 'location': value, + 'location': new_location, } ) + + if new_location == old_location: + return + # Don't validate directory if user wants to add shows without creating a dir if app.ADD_SHOWS_WO_DIR or self.is_location_valid(value): - self._location = value - else: - raise ShowDirectoryNotFoundException(u'Invalid show folder!') + self._location = new_location + return + + changed_location = True + log.info('Changing show location to: {new}', {'new': new_location}) + if not os.path.isdir(new_location): + if app.CREATE_MISSING_SHOW_DIRS: + log.info(u"Show directory doesn't exist, creating it") + try: + os.mkdir(new_location) + except OSError as error: + changed_location = False + log.warning(u"Unable to create the show directory '{location}'. Error: {msg}", + {'location': new_location, 'msg': error}) + else: + log.info(u'New show directory created') + helpers.chmod_as_parent(new_location) + else: + changed_location = False + log.warning("New location '{location}' does not exist. " + "Enable setting '(Config - Postprocessing) Create missing show dirs'", {'location': new_location}) + + # Save new location only if we changed it + if changed_location: + self._location = new_location + + if changed_location and os.path.isdir(new_location): + try: + app.show_queue_scheduler.action.refreshShow(self) + except CantRefreshShowException as error: + log.warning("Unable to refresh show '{show}'. Error: {error}", + {'show': self.name, 'error': error.message}) + + @property + def indexer_name(self): + """Return the indexer name identifier. Example: tvdb.""" + return indexerConfig[self.indexer].get('identifier') + + @property + def indexer_slug(self): + """Return the slug name of the series. Example: tvdb1234.""" + return indexer_id_to_slug(self.indexer, self.series_id) @property def current_qualities(self): @@ -432,7 +467,7 @@ def default_ep_status_name(self, status_name): @property def size(self): """Size of the show on disk.""" - return helpers.get_size(self.raw_location) + return helpers.get_size(self.location) def show_size(self, pretty=False): """ @@ -1685,6 +1720,7 @@ def delete_show(self, full=False): # remove entire show folder if full: try: + self.validate_location # Let's get the exception out of the way asap. log.info(u'{id}: Attempt to {action} show folder {location}', {'id': self.series_id, 'action': action, 'location': self.location}) # check first the read-only attribute @@ -1705,17 +1741,17 @@ def delete_show(self, full=False): shutil.rmtree(self.location) log.info(u'{id}: {action} show folder {location}', - {'id': self.series_id, 'action': action, 'location': self.raw_location}) + {'id': self.series_id, 'action': action, 'location': self.location}) except ShowDirectoryNotFoundException: log.warning(u'{id}: Show folder {location} does not exist. No need to {action}', - {'id': self.series_id, 'action': action, 'location': self.raw_location}) + {'id': self.series_id, 'action': action, 'location': self.location}) except OSError as error: log.warning( u'{id}: Unable to {action} {location}. Error: {error_msg}', { 'id': self.series_id, 'action': action, - 'location': self.raw_location, + 'location': self.location, 'error_msg': ex(error), } ) @@ -1887,7 +1923,7 @@ def save_to_db(self): control_value_dict = {'indexer': self.indexer, 'indexer_id': self.series_id} new_value_dict = {'show_name': self.name, - 'location': self.raw_location, # skip location validation + 'location': self.location, # skip location validation 'network': self.network, 'genre': self.genre, 'classification': self.classification, @@ -1937,7 +1973,7 @@ def __str__(self): to_return += 'indexerid: ' + str(self.series_id) + '\n' to_return += 'indexer: ' + str(self.indexer) + '\n' to_return += 'name: ' + self.name + '\n' - to_return += 'location: ' + self.raw_location + '\n' # skip location validation + to_return += 'location: ' + self.location + '\n' # skip location validation if self.network: to_return += 'network: ' + self.network + '\n' if self.airs: @@ -1966,7 +2002,7 @@ def __unicode__(self): to_return += u'indexerid: {0}\n'.format(self.series_id) to_return += u'indexer: {0}\n'.format(self.indexer) to_return += u'name: {0}\n'.format(self.name) - to_return += u'location: {0}\n'.format(self.raw_location) # skip location validation + to_return += u'location: {0}\n'.format(self.location) # skip location validation if self.network: to_return += u'network: {0}\n'.format(self.network) if self.airs: @@ -2028,7 +2064,7 @@ def to_json(self, detailed=True, fetch=False): data['country_codes'] = self.imdb_countries # e.g. ['it', 'fr'] data['plot'] = self.plot or self.imdb_plot data['config'] = {} - data['config']['location'] = self.raw_location + data['config']['location'] = self.location data['config']['qualities'] = {} data['config']['qualities']['allowed'] = self.qualities_allowed data['config']['qualities']['preferred'] = self.qualities_preferred