diff --git a/data/interfaces/default/displayShow.tmpl b/data/interfaces/default/displayShow.tmpl index 1d451a8de176b7bf7bbde1e4ee9b7026bbfae195..5ae71359bd6f245ff592fe258c41c0932e2f9b42 100644 --- a/data/interfaces/default/displayShow.tmpl +++ b/data/interfaces/default/displayShow.tmpl @@ -84,7 +84,7 @@ replace with: <b><%=", ".join([Quality.qualityStrings[x] for x in bestQualities] #end if </td></tr> <tr><td class="showLegend">Language:</td><td><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="" /> $show.lang</td></tr> - <tr><td class="showLegend">Flatten files (no folders): </td><td><img src="$sbRoot/images/#if $show.seasonfolders == 0 or not $sickbeard.NAMING_FORCE_FOLDERS then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> + <tr><td class="showLegend">Flatten files (no folders): </td><td><img src="$sbRoot/images/#if $show.flatten_folders == 1 or $sickbeard.NAMING_FORCE_FOLDERS then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> <tr><td class="showLegend">Paused: </td><td><img src="$sbRoot/images/#if int($show.paused) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> <tr><td class="showLegend">Air-by-Date: </td><td><img src="$sbRoot/images/#if int($show.air_by_date) == 1 then "yes16.png\" alt=\"Y" else "no16.png\" alt=\"N"#" width="16" height="16" /></td></tr> </table> diff --git a/data/interfaces/default/editShow.tmpl b/data/interfaces/default/editShow.tmpl index 478a5167eecd19fb9d52ec20deb05b88e8f0209b..57a9f15d448553fe69518bde8b780e0299d74c9b 100644 --- a/data/interfaces/default/editShow.tmpl +++ b/data/interfaces/default/editShow.tmpl @@ -65,7 +65,7 @@ Note: This will only affect the language of the retrieved metadata file contents This <b>DOES NOT</b> allow Sick Beard to download non-english TV episodes!<br /> <br /> <br /> -Flatten files (no folders): <input type="checkbox" name="flatten_folders" #if $show.seasonfolders == 0 or not $sickbeard.NAMING_FORCE_FOLDERS then "checked=\"checked\"" else ""# #if $sickbeard.NAMING_FORCE_FOLDERS then "disabled=\"disabled\"" else ""#/><br /><br /> +Flatten files (no folders): <input type="checkbox" name="flatten_folders" #if $show.flatten_folders == 1 and not $sickbeard.NAMING_FORCE_FOLDERS then "checked=\"checked\"" else ""# #if $sickbeard.NAMING_FORCE_FOLDERS then "disabled=\"disabled\"" else ""#/><br /><br /> Paused: <input type="checkbox" name="paused" #if $show.paused == 1 then "checked=\"checked\"" else ""# /><br /><br /> Air by date: diff --git a/data/interfaces/default/inc_addShowOptions.tmpl b/data/interfaces/default/inc_addShowOptions.tmpl index bd0905bbcd1fb2802689be490259a2e504459d29..c568b042665b26a1eaead91f9dfc72eb8150e89d 100644 --- a/data/interfaces/default/inc_addShowOptions.tmpl +++ b/data/interfaces/default/inc_addShowOptions.tmpl @@ -14,10 +14,10 @@ </div> <div class="field-pair alt"> - <input type="checkbox" name="seasonFolders" id="seasonFolders" #if $sickbeard.SEASON_FOLDERS_DEFAULT then "checked=\"checked\"" else ""# /> - <label for="seasonFolders" class="clearfix"> - <span class="component-title">Season Folders</span> - <span class="component-desc">Store episodes in season folders?</span> + <input type="checkbox" name="flatten_folders" id="flatten_folders" #if $sickbeard.FLATTEN_FOLDERS_DEFAULT then "checked=\"checked\"" else ""# /> + <label for="flatten_folders" class="clearfix"> + <span class="component-title">Flatten Folders</span> + <span class="component-desc">Disregard sub-folders?</span> </label> </div> diff --git a/data/interfaces/default/manage.tmpl b/data/interfaces/default/manage.tmpl index 7bd6f6336c46893ecc2cd32bb952db6d066c2cd4..36598c9092a78076f155605325df4f70f1362be7 100644 --- a/data/interfaces/default/manage.tmpl +++ b/data/interfaces/default/manage.tmpl @@ -123,7 +123,7 @@ $myShowList.sort(lambda x, y: cmp(x.name, y.name)) #else: <td align="center"><span class="quality Custom">Custom</span></td> #end if - <td align="center"><img src="$sbRoot/images/#if int($curShow.seasonfolders) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> + <td align="center"><img src="$sbRoot/images/#if int($curShow.flatten_folders) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> <td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td> <td align="center">$curShow.status</td> <td align="center">$curUpdate</td> diff --git a/data/interfaces/default/manage_massEdit.tmpl b/data/interfaces/default/manage_massEdit.tmpl index 492f4e89c60fe0089e928f15e597a5acb414be3c..fe2408c6eaa2ded966ed208e7d37120662a7bbf5 100644 --- a/data/interfaces/default/manage_massEdit.tmpl +++ b/data/interfaces/default/manage_massEdit.tmpl @@ -72,12 +72,12 @@ </div> <div class="optionWrapper"> -<span class="selectTitle">Season Folders <span class="separator">*</span></span> +<span class="selectTitle">Flatten Folders <span class="separator">*</span></span> <div class="selectChoices"> - <select id="edit_season_folders" name="season_folders"> + <select id="edit_flatten_folders" name="flatten_folders"> <option value="keep">< keep ></option> - <option value="enable" #if $season_folders_value then "selected=\"selected\"" else ""#>enable</option> - <option value="disable" #if $season_folders_value == False then "selected=\"selected\"" else ""#>disable</option> + <option value="enable" #if $flatten_folders_value then "selected=\"selected\"" else ""#>enable</option> + <option value="disable" #if $flatten_folders_value == False then "selected=\"selected\"" else ""#>disable</option> </select> </div><br /> </div> diff --git a/data/interfaces/default/testRename.tmpl b/data/interfaces/default/testRename.tmpl index 67b1f06de4a4f0e32bceb6b985960faae28a0a48..eeab672a46b020021526a73fe6b5c143d30f81c0 100644 --- a/data/interfaces/default/testRename.tmpl +++ b/data/interfaces/default/testRename.tmpl @@ -40,7 +40,7 @@ <td width="1%"> <input type="checkbox" class="epCheck" id="<%=str(cur_ep_obj.season)+'x'+str(cur_ep_obj.episode)%>" name="<%=str(cur_ep_obj.season) +"x"+str(cur_ep_obj.episode) %>" /> </td> - <td align="center">$cur_ep_obj.episode</td> + <td align="center"><%= " / ".join([str(cur_ep_obj.episode)] + [str(x.episode) for x in cur_ep_obj.relatedEps]) %></td> <td width="50%">$curLoc</td> <td width="50%">$cur_ep_obj.proper_path().$curExt</td> </td> diff --git a/data/js/addShowOptions.js b/data/js/addShowOptions.js index eaa830a93af38a20a5486f9710bf5e7314b52212..5f2f2c9e9c3da78a9d2c7968a9b98d0a6f44a51c 100644 --- a/data/js/addShowOptions.js +++ b/data/js/addShowOptions.js @@ -9,7 +9,7 @@ $(document).ready(function(){ $.get(sbRoot+'/config/general/saveAddShowDefaults', {defaultStatus: $('#statusSelect').val(), anyQualities: anyQualArray.join(','), bestQualities: bestQualArray.join(','), - defaultSeasonFolders: $('#seasonFolders').prop('checked')} ); + defaultFlattenFolders: $('#flatten_folders').prop('checked')} ); $(this).attr('disabled', true); $.pnotify({ pnotify_title: 'Saved Defaults', @@ -17,7 +17,7 @@ $(document).ready(function(){ }); }); - $('#statusSelect, #qualityPreset, #seasonFolders, #anyQualities, #bestQualities').change(function(){ + $('#statusSelect, #qualityPreset, #flatten_folders, #anyQualities, #bestQualities').change(function(){ $('#saveDefaultsButton').attr('disabled', false); }); diff --git a/data/js/testRename.js b/data/js/testRename.js index 549667166c93da24fb8406e8b5754755bc762679..658ee484273cb6213816cce245864203fa508c0e 100644 --- a/data/js/testRename.js +++ b/data/js/testRename.js @@ -25,8 +25,7 @@ $(document).ready(function(){ return false url = sbRoot+'/home/doRename?show='+$('#showID').attr('value')+'&eps='+epArr.join('|') - //window.location.href = url - alert(url) + window.location.href = url }); }); \ No newline at end of file diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 89263c29ce6196a428c75b75349d4dc6ea8c7cb7..aebf5f9d819ae95a544511af501327d8bde47e9b 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -123,8 +123,7 @@ NAME_FORMATTING = r'%RN/%0Sx%0E - %E.N (%RG)' QUALITY_DEFAULT = None STATUS_DEFAULT = None -SEASON_FOLDERS_FORMAT = None -SEASON_FOLDERS_DEFAULT = None +FLATTEN_FOLDERS_DEFAULT = None PROVIDER_ORDER = [] NAMING_MULTI_EP = None @@ -378,7 +377,7 @@ def initialize(consoleLogging=True): showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, showList, loadingShowList, \ NZBS, NZBS_UID, NZBS_HASH, EZRSS, TVTORRENTS, TVTORRENTS_DIGEST, TVTORRENTS_HASH, TORRENT_DIR, USENET_RETENTION, SOCKET_TIMEOUT, \ SEARCH_FREQUENCY, DEFAULT_SEARCH_FREQUENCY, BACKLOG_SEARCH_FREQUENCY, \ - QUALITY_DEFAULT, SEASON_FOLDERS_FORMAT, SEASON_FOLDERS_DEFAULT, STATUS_DEFAULT, \ + QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, STATUS_DEFAULT, \ GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, \ USE_GROWL, GROWL_HOST, GROWL_PASSWORD, USE_PROWL, PROWL_NOTIFY_ONSNATCH, PROWL_NOTIFY_ONDOWNLOAD, PROWL_API, PROWL_PRIORITY, PROG_DIR, NZBMATRIX, NZBMATRIX_USERNAME, \ USE_PYTIVO, PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_UPDATE_LIBRARY, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \ @@ -491,8 +490,7 @@ def initialize(consoleLogging=True): QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD) STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED) VERSION_NOTIFY = check_setting_int(CFG, 'General', 'version_notify', 1) - SEASON_FOLDERS_FORMAT = check_setting_str(CFG, 'General', 'season_folders_format', 'Season %02d') - SEASON_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'season_folders_default', 0)) + FLATTEN_FOLDERS_DEFAULT = bool(check_setting_int(CFG, 'General', 'flatten_folders_default', 0)) PROVIDER_ORDER = check_setting_str(CFG, 'General', 'provider_order', '').split() @@ -994,8 +992,7 @@ def save_config(): new_config['General']['download_propers'] = int(DOWNLOAD_PROPERS) new_config['General']['quality_default'] = int(QUALITY_DEFAULT) new_config['General']['status_default'] = int(STATUS_DEFAULT) - new_config['General']['season_folders_format'] = SEASON_FOLDERS_FORMAT - new_config['General']['season_folders_default'] = int(SEASON_FOLDERS_DEFAULT) + new_config['General']['flatten_folders_default'] = int(FLATTEN_FOLDERS_DEFAULT) new_config['General']['provider_order'] = ' '.join([x.getID() for x in providers.sortedProviderList()]) new_config['General']['version_notify'] = int(VERSION_NOTIFY) new_config['General']['naming_pattern'] = NAMING_PATTERN diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index 1edad2f320d9e96a70cfff467cee4a6a6cb4f687..4ce58461cf28761b198f7119bc6982fa961eb09f 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -469,3 +469,23 @@ class AddSizeAndSceneNameFields(FixAirByDateSetting): self.incDBVersion() +class RenameSeasonFolders(AddSizeAndSceneNameFields): + + def test(self): + return self.checkDBVersion() >= 11 + + def execute(self): + + self.connection.action("ALTER TABLE tv_shows RENAME TO tmp_tv_shows") + + self.connection.action("CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, location TEXT, show_name TEXT, tvdb_id NUMERIC, network TEXT, genre TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, tvr_id NUMERIC, tvr_name TEXT, air_by_date NUMERIC, lang TEXT)") + + sql = "INSERT INTO tv_shows(show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, tvr_id, tvr_name, air_by_date, lang) SELECT show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status, seasonfolders, paused, startyear, tvr_id, tvr_name, air_by_date, lang FROM tmp_tv_shows" + self.connection.action(sql) + + self.connection.action("UPDATE tv_shows SET flatten_folders = 2 WHERE flatten_folders = 1") + self.connection.action("UPDATE tv_shows SET flatten_folders = 1 WHERE flatten_folders = 0") + self.connection.action("UPDATE tv_shows SET flatten_folders = 0 WHERE flatten_folders = 2") + self.connection.action("DROP TABLE tmp_tv_shows") + + self.incDBVersion() diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index a0c6d47335053f22376c4f57535fdaed34ad4a46..6b4c4f428b7d958b20d7f3f13c918b28d64ebd0b 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -406,22 +406,72 @@ def moveFile(srcFile, destFile): copyFile(srcFile, destFile) ek.ek(os.unlink, srcFile) -def rename_file(old_path, new_name): +def make_dirs(path): + """ + Creates any folders that are missing and assigns them the permissions of their + parents + """ + + # strip off the file name if there is one + path = os.path.dirname(path) - old_path_list = ek.ek(os.path.split, old_path) - old_file_ext = os.path.splitext(old_path_list[1])[1] + sofar = '' + folder_list = path.split(os.path.sep) - new_path = ek.ek(os.path.join, old_path_list[0], sanitizeFileName(new_name) + old_file_ext) + # look through each subfolder and make sure they all exist + for cur_folder in folder_list: + sofar += cur_folder + os.path.sep; - logger.log(u"Renaming from " + old_path + " to " + new_path) + # if it exists then just keep walking down the line + if ek.ek(os.path.isdir, sofar): + continue + try: + ek.ek(os.mkdir, sofar) + chmodAsParent(sofar) + except OSError, IOError: + return False + + return True + +def rename_ep_file(cur_path, new_path): + """ + Creates all folders needed to move a file to its new location, renames it, then cleans up any folders + left that are now empty. + + cur_path: The absolute path to the file you want to move/rename + new_path: The absolute path to the destination for the file WITHOUT THE EXTENSION + """ + + logger.log(u"Renaming file from "+cur_path+" to "+new_path) + + new_dest_dir, new_dest_name = os.path.split(new_path) #@UnusedVariable + cur_file_name, cur_file_ext = os.path.splitext(cur_path) #@UnusedVariable + + # put the extension on the incoming file + new_path += cur_file_ext + + make_dirs(new_path) + + # move the file try: - ek.ek(os.rename, old_path, new_path) + ek.ek(os.rename, cur_path, new_path) except (OSError, IOError), e: - logger.log(u"Failed renaming " + old_path + " to " + new_path + ": " + ex(e), logger.ERROR) + logger.log(u"Failed renaming " + cur_path + " to " + new_path + ": " + ex(e), logger.ERROR) return False - - return new_path + + # clean up any old folders that are empty + check_empty_dir = ek.ek(os.path.dirname, cur_path) + while not os.listdir(check_empty_dir): + logger.log(u"Deleting empty folder: "+check_empty_dir) + try: + os.remove(check_empty_dir) + except (WindowsError, OSError): + logger.log(u"Unable to delete "+check_empty_dir, logger.WARNING) + break + check_empty_dir = os.path.basename(check_empty_dir) + + return True def chmodAsParent(childPath): if os.name == 'nt' or os.name == 'ce': diff --git a/sickbeard/naming.py b/sickbeard/naming.py index 8b5c3fbc052989958c6ef239449697fcbc6610c8..45b63b7e9bac18672788d8151c4497fd982ae9c2 100644 --- a/sickbeard/naming.py +++ b/sickbeard/naming.py @@ -126,4 +126,4 @@ def test_name(pattern, multi=None): ep = _generate_sample_ep(multi) - return {'name': ep.formatted_filename(pattern, multi), 'dir': ep.formatted_dir(pattern, multi)} + return {'name': ep.formatted_filename(pattern, multi), 'dir': ep.formatted_dir(pattern, multi)} \ No newline at end of file diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index 855f7e8afd0ed87416938834bc2b0d0ffce5f325..1b99f5692e7b959cb168bb62531a4bfba9a3b03b 100755 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -112,6 +112,9 @@ class PostProcessor(object): return PostProcessor.DOESNT_EXIST def _list_associated_files(self, file_path): + """ + Returns the absolute path to all files that share the same name but different extension with the given file + """ if not file_path: return [] @@ -120,6 +123,10 @@ class PostProcessor(object): base_name = file_path.rpartition('.')[0]+'.' + # don't strip it all and use cwd by accident + if not base_name: + return [] + # don't confuse glob with chars we didn't mean to use base_name = re.sub(r'[\[\]\*\?]', r'[\g<0>]', base_name) @@ -235,41 +242,6 @@ class PostProcessor(object): self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_copy) - def _find_ep_destination_folder(self, ep_obj): - - # if we're supposed to put it in a season folder then figure out what folder to use - season_folder = '' - if ep_obj.show.seasonfolders: - - # search the show dir for season folders - for curDir in ek.ek(os.listdir, ep_obj.show.location): - - if not ek.ek(os.path.isdir, ek.ek(os.path.join, ep_obj.show.location, curDir)): - continue - - # if it's a season folder, check if it's the one we want - match = re.match(".*season\s*(\d+)", curDir, re.IGNORECASE) - if match: - # if it's the correct season folder then stop looking - if int(match.group(1)) == int(ep_obj.season): - season_folder = curDir - break - - # if we couldn't find the right one then just use the season folder defaut format - if season_folder == '': - # for air-by-date shows use the year as the season folder - if ep_obj.show.air_by_date: - season_folder = str(ep_obj.airdate.year) - else: - try: - season_folder = sickbeard.SEASON_FOLDERS_FORMAT % (ep_obj.season) - except TypeError: - logger.log(u"Error: Your season folder format is incorrect, try setting it back to the default") - - dest_folder = ek.ek(os.path.join, ep_obj.show.location, season_folder) - - return dest_folder - def _history_lookup(self): """ Look up the NZB name in the history and see if it contains a record for self.nzb_name @@ -670,20 +642,17 @@ class PostProcessor(object): # find the destination folder try: - dest_path = self._find_ep_destination_folder(ep_obj) + proper_path = ep_obj.proper_path() + proper_absolute_path = ek.ek(os.path.join, ep_obj.show.location, proper_path) + + dest_path = ek.ek(os.path.dirname, proper_absolute_path) except exceptions.ShowDirNotFoundException: raise exceptions.PostProcessingFailed(u"Unable to post-process an episode if the show dir doesn't exist, quitting") self._log(u"Destination folder for this episode: "+dest_path, logger.DEBUG) - - # if the dir doesn't exist (new season folder) then make it - if not ek.ek(os.path.isdir, dest_path): - self._log(u"Season folder didn't exist, creating it", logger.DEBUG) - try: - ek.ek(os.mkdir, dest_path) - helpers.chmodAsParent(dest_path) - except OSError, IOError: - raise exceptions.PostProcessingFailed("Unable to create the episode's destination folder: "+dest_path) + + # create any folders we need + helpers.make_dirs(dest_path) # update the statuses before we rename so the quality goes into the name properly for cur_ep in [ep_obj] + ep_obj.relatedEps: @@ -694,7 +663,7 @@ class PostProcessor(object): # figure out the base name of the resulting episode file if sickbeard.RENAME_EPISODES: orig_extension = self.file_name.rpartition('.')[-1] - new_base_name = helpers.sanitizeFileName(ep_obj.prettyName()) + new_base_name = ek.ek(os.path.basename, ep_obj.proper_path) new_file_name = new_base_name + '.' + orig_extension else: diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py index cbfa169fda94374bc1a16d305525d420bd979b30..039b2227de3f3ab79f11abf05ca79009845891c6 100644 --- a/sickbeard/show_queue.py +++ b/sickbeard/show_queue.py @@ -115,8 +115,8 @@ class ShowQueue(generic_queue.GenericQueue): return queueItemObj - def addShow(self, tvdb_id, showDir, default_status=None, quality=None, season_folders=None, lang="en"): - queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, season_folders, lang) + def addShow(self, tvdb_id, showDir, default_status=None, quality=None, flatten_folders=None, lang="en"): + queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, flatten_folders, lang) self.add_item(queueItemObj) @@ -165,13 +165,13 @@ class ShowQueueItem(generic_queue.QueueItem): class QueueItemAdd(ShowQueueItem): - def __init__(self, tvdb_id, showDir, default_status, quality, season_folders, lang): + def __init__(self, tvdb_id, showDir, default_status, quality, flatten_folders, lang): self.tvdb_id = tvdb_id self.showDir = showDir self.default_status = default_status self.quality = quality - self.season_folders = season_folders + self.flatten_folders = flatten_folders self.lang = lang self.show = None @@ -242,7 +242,7 @@ class QueueItemAdd(ShowQueueItem): # set up initial values self.show.location = self.showDir self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT - self.show.seasonfolders = self.season_folders if self.season_folders != None else sickbeard.SEASON_FOLDERS_DEFAULT + self.show.flatten_folders = self.flatten_folders if self.flatten_folders != None else sickbeard.FLATTEN_FOLDERS_DEFAULT self.show.paused = False # be smartish about this @@ -347,7 +347,8 @@ class QueueItemRename(ShowQueueItem): logger.log(u"Performing rename on "+self.show.name) - self.show.fixEpisodeNames() + for cur_ep_obj in self.show.loadEpisodesFromDir(): + cur_ep_obj.rename() self.inProgress = False diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 6e139909b2b988880cef4e4d3062df62ba091636..1d8bfa09620366a05f0da880d73a4ef7a8a462a0 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -43,7 +43,7 @@ from sickbeard import encodingKludge as ek from common import Quality, Overview from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, ARCHIVED, IGNORED, UNAIRED, WANTED, SKIPPED, UNKNOWN -from common import NAMING_DUPLICATE, NAMING_EXTEND, NAMING_REPEAT +from common import NAMING_DUPLICATE, NAMING_EXTEND class TVShow(object): @@ -59,7 +59,7 @@ class TVShow(object): self.genre = "" self.runtime = 0 self.quality = int(sickbeard.QUALITY_DEFAULT) - self.seasonfolders = int(sickbeard.SEASON_FOLDERS_DEFAULT) + self.flatten_folders = int(sickbeard.FLATTEN_FOLDERS_DEFAULT) self.status = "" self.airs = "" @@ -463,7 +463,8 @@ class TVShow(object): if rootEp == None: rootEp = curEp else: - rootEp.relatedEps.append(curEp) + if curEp not in rootEp.relatedEps: + rootEp.relatedEps.append(curEp) # if it's a new file then if not same_file: @@ -555,7 +556,7 @@ class TVShow(object): self.air_by_date = 0 self.quality = int(sqlResults[0]["quality"]) - self.seasonfolders = int(sqlResults[0]["seasonfolders"]) + self.flatten_folders = int(sqlResults[0]["flatten_folders"]) self.paused = int(sqlResults[0]["paused"]) self._location = sqlResults[0]["location"] @@ -754,80 +755,6 @@ class TVShow(object): curEp.hastbn = False curEp.saveToDB() - - - def fixEpisodeNames(self): - - if not os.path.isdir(self._location): - logger.log(str(self.tvdbid) + ": Show dir doesn't exist, can't rename episodes") - return - - # load episodes from my folder - self.loadEpisodesFromDir() - - logger.log(str(self.tvdbid) + ": Loading all episodes with a location from the database") - - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != ''", [self.tvdbid]) - - # build list of locations - fileLocations = {} - for epResult in sqlResults: - goodLoc = os.path.normpath(epResult["location"]) - goodSeason = int(epResult["season"]) - goodEpisode = int(epResult["episode"]) - if fileLocations.has_key(goodLoc): - fileLocations[goodLoc].append((goodSeason, goodEpisode)) - else: - fileLocations[goodLoc] = [(goodSeason, goodEpisode)] - - logger.log(u"File results: " + str(fileLocations), logger.DEBUG) - - for curLocation in fileLocations: - - epList = fileLocations[curLocation] - - # get the root episode and add all related episodes to it - rootEp = None - for myEp in epList: - curEp = self.getEpisode(myEp[0], myEp[1]) - if rootEp == None: - rootEp = curEp - rootEp.relatedEps = [] - else: - rootEp.relatedEps.append(curEp) - - goodName = rootEp.prettyName() - actualName = os.path.splitext(os.path.basename(curLocation)) - - if goodName == actualName[0]: - logger.log(str(self.tvdbid) + ": File " + rootEp.location + " is already named correctly, skipping", logger.DEBUG) - continue - - with rootEp.lock: - result = helpers.rename_file(rootEp.location, rootEp.prettyName()) - if result != False: - rootEp.location = result - for relEp in rootEp.relatedEps: - relEp.location = result - - fileList = postProcessor.PostProcessor(curLocation)._list_associated_files(curLocation) - logger.log(u"Files associated to "+curLocation+": "+str(fileList), logger.DEBUG) - - for file in fileList: - result = helpers.rename_file(file, rootEp.prettyName()) - if result == False: - logger.log(str(self.tvdbid) + ": Unable to rename file "+file, logger.ERROR) - - for curEp in [rootEp]+rootEp.relatedEps: - curEp.checkForMetaFiles() - - with rootEp.lock: - rootEp.saveToDB() - for relEp in rootEp.relatedEps: - relEp.saveToDB() - - def saveToDB(self): logger.log(str(self.tvdbid) + ": Saving show info to database", logger.DEBUG) @@ -844,7 +771,7 @@ class TVShow(object): "quality": self.quality, "airs": self.airs, "status": self.status, - "seasonfolders": self.seasonfolders, + "flatten_folders": self.flatten_folders, "paused": self.paused, "air_by_date": self.air_by_date, "startyear": self.startyear, @@ -1405,7 +1332,7 @@ class TVEpisode(object): def prettyName (self, naming_show_name=None, naming_ep_type=None, naming_multi_ep_type=None, naming_ep_name=None, naming_sep_type=None, naming_use_periods=None, naming_quality=None): - return self._formatted_string('%SN - %Sx%0E - %EN') + return self._format_string('%SN - %Sx%0E - %EN') def _ep_name(self): """ @@ -1454,7 +1381,7 @@ class TVEpisode(object): ep_name = self._ep_name() def dot(name): - return re.sub('[_ -]','.', name) + return helpers.sanitizeSceneName(name) def us(name): return re.sub('[ -]','_', name) @@ -1580,20 +1507,20 @@ class TVEpisode(object): # do the replacements for cur_replacement in sorted(replace_map.keys(), reverse=True): - result_name = result_name.replace(cur_replacement, replace_map[cur_replacement]) - result_name = result_name.replace(cur_replacement.lower(), replace_map[cur_replacement].lower()) + result_name = result_name.replace(cur_replacement, helpers.sanitizeFileName(replace_map[cur_replacement])) + result_name = result_name.replace(cur_replacement.lower(), helpers.sanitizeFileName(replace_map[cur_replacement].lower())) return result_name def proper_path(self): """ - Figures out where this episode SHOULD live according to the renaming rules + Figures out the path where this episode SHOULD live according to the renaming rules, relative from the show dir """ result = self.formatted_filename() # as long as they don't want us to flatten it and we're not FORCED to flatten it then append the dir - if self.show.seasonfolders or sickbeard.NAMING_FORCE_FOLDERS: + if not self.show.flatten_folders or sickbeard.NAMING_FORCE_FOLDERS: result = ek.ek(os.path.join, self.formatted_dir(), result) return result @@ -1628,3 +1555,50 @@ class TVEpisode(object): name_groups = re.split(r'[\\/]', pattern) return self._format_string(name_groups[-1], multi) + + def rename(self): + + proper_path = self.proper_path() + absolute_proper_path = ek.ek(os.path.join, self.show.location, proper_path) + absolute_current_path_no_ext, file_ext = os.path.splitext(self.location) + + current_path = absolute_current_path_no_ext + + if absolute_current_path_no_ext.startswith(self.show.location): + current_path = absolute_current_path_no_ext[len(self.show.location):] + + logger.log(u"Renaming/moving episode from the base path "+self.location+" to "+absolute_proper_path, logger.DEBUG) + + # if it's already named correctly then don't do anything + if proper_path == current_path: + logger.log(str(self.tvdbid) + ": File " + self.location + " is already named correctly, skipping", logger.DEBUG) + return + + related_files = postProcessor.PostProcessor(self.location)._list_associated_files(self.location) + logger.log(u"Files associated to "+self.location+": "+str(related_files), logger.DEBUG) + + # move the ep file + result = helpers.rename_ep_file(self.location, absolute_proper_path) + + # move related files + for cur_related_file in related_files: + cur_result = helpers.rename_ep_file(cur_related_file, absolute_proper_path) + if cur_result == False: + logger.log(str(self.tvdbid) + ": Unable to rename file "+cur_related_file, logger.ERROR) + + # save the ep + with self.lock: + if result != False: + self.location = absolute_proper_path + file_ext + for relEp in self.relatedEps: + relEp.location = absolute_proper_path + file_ext + + # in case something changed with the metadata just do a quick check + for curEp in [self]+self.relatedEps: + curEp.checkForMetaFiles() + + # save any changes to the database + with self.lock: + self.saveToDB() + for relEp in self.relatedEps: + relEp.saveToDB() diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 5de587dbb8f2a7b6e9dde055c1067cb967ebc853..b24efa72f87439295c4e5316aef2e9d5f3498343 100755 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -352,8 +352,8 @@ class Manage: if showObj: showList.append(showObj) - season_folders_all_same = True - last_season_folders = None + flatten_folders_all_same = True + last_flatten_folders = None paused_all_same = True last_paused = None @@ -377,11 +377,11 @@ class Manage: else: last_paused = curShow.paused - if season_folders_all_same: - if last_season_folders not in (None, curShow.seasonfolders): - season_folders_all_same = False + if flatten_folders_all_same: + if last_flatten_folders not in (None, curShow.flatten_folders): + flatten_folders_all_same = False else: - last_season_folders = curShow.seasonfolders + last_flatten_folders = curShow.flatten_folders if quality_all_same: if last_quality not in (None, curShow.quality): @@ -391,14 +391,14 @@ class Manage: t.showList = toEdit t.paused_value = last_paused if paused_all_same else None - t.season_folders_value = last_season_folders if season_folders_all_same else None + t.flatten_folders_value = last_flatten_folders if flatten_folders_all_same else None t.quality_value = last_quality if quality_all_same else None t.root_dir_list = root_dir_list return _munge(t) @cherrypy.expose - def massEditSubmit(self, paused=None, season_folders=None, quality_preset=False, + def massEditSubmit(self, paused=None, flatten_folders=None, quality_preset=False, anyQualities=[], bestQualities=[], toEdit=None, *args, **kwargs): dir_map = {} @@ -431,16 +431,16 @@ class Manage: new_paused = True if paused == 'enable' else False new_paused = 'on' if new_paused else 'off' - if season_folders == 'keep': - new_season_folders = showObj.seasonfolders + if flatten_folders == 'keep': + new_flatten_folders = showObj.flatten_folders else: - new_season_folders = True if season_folders == 'enable' else False - new_season_folders = 'on' if new_season_folders else 'off' + new_flatten_folders = True if flatten_folders == 'enable' else False + new_flatten_folders = 'on' if new_flatten_folders else 'off' if quality_preset == 'keep': anyQualities, bestQualities = Quality.splitQuality(showObj.quality) - curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, new_season_folders, new_paused, directCall=True) + curErrors += Home().editShow(curShow, new_show_dir, anyQualities, bestQualities, new_flatten_folders, new_paused, directCall=True) if curErrors: logger.log(u"Errors: "+str(curErrors), logger.ERROR) @@ -611,7 +611,7 @@ class ConfigGeneral: sickbeard.ROOT_DIRS = rootDirString @cherrypy.expose - def saveAddShowDefaults(self, defaultSeasonFolders, defaultStatus, anyQualities, bestQualities): + def saveAddShowDefaults(self, defaultFlattenFolders, defaultStatus, anyQualities, bestQualities): if anyQualities: anyQualities = anyQualities.split(',') @@ -628,12 +628,12 @@ class ConfigGeneral: sickbeard.STATUS_DEFAULT = int(defaultStatus) sickbeard.QUALITY_DEFAULT = int(newQuality) - if defaultSeasonFolders == "true": - defaultSeasonFolders = 1 + if defaultFlattenFolders == "true": + defaultFlattenFolders = 1 else: - defaultSeasonFolders = 0 + defaultFlattenFolders = 0 - sickbeard.SEASON_FOLDERS_DEFAULT = int(defaultSeasonFolders) + sickbeard.FLATTEN_FOLDERS_DEFAULT = int(defaultFlattenFolders) @cherrypy.expose def generateKey(self): @@ -1576,7 +1576,7 @@ class NewHomeAddShows: @cherrypy.expose def addNewShow(self, whichSeries=None, tvdbLang="en", rootDir=None, defaultStatus=None, - anyQualities=None, bestQualities=None, seasonFolders=None, fullShowPath=None, + anyQualities=None, bestQualities=None, flatten_folders=None, fullShowPath=None, other_shows=None, skipShow=None): """ Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are @@ -1638,10 +1638,10 @@ class NewHomeAddShows: helpers.chmodAsParent(show_dir) # prepare the inputs for passing along - if seasonFolders == "on": - seasonFolders = 1 + if flatten_folders == "on": + flatten_folders = 1 else: - seasonFolders = 0 + flatten_folders = 0 if not anyQualities: anyQualities = [] @@ -1654,7 +1654,7 @@ class NewHomeAddShows: newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, seasonFolders, tvdbLang) #@UndefinedVariable + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, flatten_folders, tvdbLang) #@UndefinedVariable ui.notifications.message('Show added', 'Adding the specified show into '+show_dir) return finishAddShow() @@ -1725,7 +1725,7 @@ class NewHomeAddShows: show_dir, tvdb_id, show_name = cur_show # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.SEASON_FOLDERS_DEFAULT) #@UndefinedVariable + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.FLATTEN_FOLDERS_DEFAULT) #@UndefinedVariable num_added += 1 if num_added: @@ -2152,9 +2152,11 @@ class Home: return _munge(t) if flatten_folders == "on": - flatten_folders = 0 - else: flatten_folders = 1 + else: + flatten_folders = 0 + + logger.log(u"flatten folders: "+str(flatten_folders)) if paused == "on": paused = 1 @@ -2188,8 +2190,9 @@ class Home: newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) showObj.quality = newQuality - if bool(showObj.seasonfolders) != bool(flatten_folders): - showObj.seasonfolders = flatten_folders + # reversed for now + if bool(showObj.flatten_folders) != bool(flatten_folders): + showObj.flatten_folders = flatten_folders try: sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable except exceptions.CantRefreshException, e: @@ -2327,25 +2330,6 @@ class Home: logger.log(u"Plex library update failed for host " + sickbeard.PLEX_HOST, logger.ERROR) redirect('/home') - - @cherrypy.expose - def fixEpisodeNames(self, show=None): - - if show == None: - return _genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj == None: - return _genericMessage("Error", "Unable to find the specified show") - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable - return _genericMessage("Error", "Show is still being added, wait until it is finished before you rename files") - - showObj.fixEpisodeNames() - - redirect("/home/displayShow?show=" + show) - @cherrypy.expose def setStatus(self, show=None, eps=None, status=None, direct=False): @@ -2444,10 +2428,15 @@ class Home: ep_obj_list = [] - for cur_ep_obj in showObj.getAllEpisodes(): - if not cur_ep_obj.location: - continue + myDB = db.DBConnection() + ep_list = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND location != '' GROUP BY location ORDER BY season*1000+episode", [show]) + + for cur_ep in ep_list: + + # get the episode object + cur_ep_obj = showObj.makeEpFromFile(cur_ep["location"]) + # figure out the path vars cur_path = cur_ep_obj.location[len(showObj.location)+1:] cur_ext = cur_path.rsplit('.')[-1] @@ -2460,6 +2449,45 @@ class Home: return _munge(t) + @cherrypy.expose + def doRename(self, show=None, eps=None): + + if show == None or eps == None: + errMsg = "You must specify a show and at least one episode" + return _genericMessage("Error", errMsg) + + show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if show_obj == None: + errMsg = "Error", "Show not in show list" + return _genericMessage("Error", errMsg) + + myDB = db.DBConnection() + + if eps == None: + redirect("/home/displayShow?show=" + show) + + for curEp in eps.split('|'): + + epInfo = curEp.split('x') + + # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database + ep_result = myDB.select("SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", [show, epInfo[0], epInfo[1]]) + if not ep_result: + logger.log(u"Unable to find an episode for "+curEp+", skipping", logger.WARNING) + continue + related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE location = ? AND episode != ?", [ep_result[0]["location"], epInfo[1]]) + + root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) + for cur_related_ep in related_eps_result: + related_ep_obj = show_obj.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) + if related_ep_obj not in root_ep_obj.relatedEps: + root_ep_obj.relatedEps.append(related_ep_obj) + + root_ep_obj.rename() + + redirect("/home/displayShow?show=" + show) + @cherrypy.expose def searchEpisode(self, show=None, season=None, episode=None):