diff --git a/tvnamer/cliarg_parser.py b/tvnamer/cliarg_parser.py index 2b1fbb4..9e2e711 100644 --- a/tvnamer/cliarg_parser.py +++ b/tvnamer/cliarg_parser.py @@ -66,6 +66,12 @@ def getCommandlineParser(defaults): g.add_option("-m", "--move", action="store_true", dest="move_files_enable", help = "Move files to destination specified in config or with --movedestination argument") g.add_option("--not-move", action="store_false", dest="move_files_enable", help = "Files will remain in current directory") + g.add_option("-C", "--copy", action="store_true", dest="always_copy", help = "Copy files instead of renaming") + g.add_option("--not-copy", action="store_false", dest="always_copy", help = "Files will not be copied") + + g.add_option("-l", "--link", action="store_true", dest="always_hardlink", help = "Hardlink instead of renaming") + g.add_option("--not-link", action="store_false", dest="always_hardlink", help = "Files will not all be hardlink") + g.add_option("--force-move", action="store_true", dest = "overwrite_destination_on_move", help = "Force move and potentially overwrite existing files in destination folder") g.add_option("--force-rename", action="store_true", dest = "overwrite_destination_on_rename", help = "Force rename source file") diff --git a/tvnamer/config_defaults.py b/tvnamer/config_defaults.py index 15d572a..275f1ab 100644 --- a/tvnamer/config_defaults.py +++ b/tvnamer/config_defaults.py @@ -14,7 +14,7 @@ 'batch': False, # Fail if error finding show data (thetvdb.com is down etc) - # Only functions when always_rename is True + # Only functions when always_rename or always_hardlink are True 'skip_file_on_error': True, # Fail if error finding show data (thetvdb.com is down etc) @@ -146,6 +146,18 @@ # Allow user to copy files to specified move location without renaming files. 'move_files_only': False, + # Force the move-files feature to always copy the file. + 'always_copy': False, + + # If True, instead of copying files to be copied on the same partition, + # a hardlink will be created. + 'prefer_hardlink_to_copy': False, + + # Forces files to be hard-linked. Will raise an error if it is not possible + # (i.e. different partition, unsuported, etc.). + # This option is incompatible with always_move + 'always_hardlink': False, + # Patterns to parse input filenames with 'filename_patterns': [ # [group] Show - 01-02 [crc] diff --git a/tvnamer/main.py b/tvnamer/main.py index c8cd025..8a26e22 100644 --- a/tvnamer/main.py +++ b/tvnamer/main.py @@ -99,7 +99,7 @@ def doMoveFile(cnamer, destDir = None, destFilepath = None, getPathPreview = Fal if (destDir is None and destFilepath is None) or (destDir is not None and destFilepath is not None): raise ValueError("Specify only destDir or destFilepath") - if not Config['move_files_enable']: + if not (Config['move_files_enable'] or Config['always_copy'] or Config['always_hardlink']): raise ValueError("move_files feature is disabled but doMoveFile was called") if Config['move_files_destination'] is None: @@ -110,6 +110,8 @@ def doMoveFile(cnamer, destDir = None, destFilepath = None, getPathPreview = Fal new_path = destDir, new_fullpath = destFilepath, always_move = Config['always_move'], + always_copy = Config['always_copy'], + always_hardlink=Config['always_hardlink'], leave_symlink = Config['leave_symlink'], getPathPreview = getPathPreview, force = Config['overwrite_destination_on_move']) @@ -224,12 +226,14 @@ def processFile(tvdb_instance, episode): p("%s will be moved to %s" % (newName, getMoveDestination(episode))) return elif Config['always_rename']: - doRenameFile(cnamer, newName) - if Config['move_files_enable']: + if Config['move_files_enable'] or Config['always_copy'] or Config['always_hardlink']: if Config['move_files_destination_is_filepath']: doMoveFile(cnamer = cnamer, destFilepath = getMoveDestination(episode)) else: doMoveFile(cnamer = cnamer, destDir = getMoveDestination(episode)) + doRenameFile(cnamer, newName) + else: + doRenameFile(cnamer, newName) return ans = confirm("Rename?", options = ['y', 'n', 'a', 'q'], default = 'y') @@ -249,31 +253,30 @@ def processFile(tvdb_instance, episode): else: p("Invalid input, skipping") - if shouldRename: - doRenameFile(cnamer, newName) + if shouldRename: + if Config['move_files_enable'] or Config['always_copy'] or Config['always_hardlink']: + newPath = getMoveDestination(episode) + if Config['dry_run']: + p("%s will be moved to %s" % (newName, getMoveDestination(episode))) + return - if shouldRename and Config['move_files_enable']: - newPath = getMoveDestination(episode) - if Config['dry_run']: - p("%s will be moved to %s" % (newName, getMoveDestination(episode))) - return + if Config['move_files_destination_is_filepath']: + doMoveFile(cnamer = cnamer, destFilepath = newPath, getPathPreview = True) + else: + doMoveFile(cnamer = cnamer, destDir = newPath, getPathPreview = True) - if Config['move_files_destination_is_filepath']: - doMoveFile(cnamer = cnamer, destFilepath = newPath, getPathPreview = True) - else: - doMoveFile(cnamer = cnamer, destDir = newPath, getPathPreview = True) + if not Config['batch'] and Config['move_files_confirmation']: + ans = confirm("Move file?", options = ['y', 'n', 'q'], default = 'y') + else: + ans = 'y' - if not Config['batch'] and Config['move_files_confirmation']: - ans = confirm("Move file?", options = ['y', 'n', 'q'], default = 'y') - else: - ans = 'y' - - if ans == 'y': - p("Moving file") - doMoveFile(cnamer, newPath) - elif ans == 'q': - p("Quitting") - raise UserAbort("user exited with q") + if ans == 'y': + p("Moving file") + doMoveFile(cnamer, newPath) + elif ans == 'q': + p("Quitting") + raise UserAbort("user exited with q") + doRenameFile(cnamer, newName) def findFiles(paths): diff --git a/tvnamer/utils.py b/tvnamer/utils.py index 360e4f9..1d2849d 100644 --- a/tvnamer/utils.py +++ b/tvnamer/utils.py @@ -1019,6 +1019,14 @@ def symlink_file(target, name): os.symlink(target, name) +def hardlink_file(old, new): + """Creates a hard link from the 'old' file at a 'new' target path with same permissions + """ + p("hardlink %s to %s" % (old, new)) + os.link(old, new) + shutil.copystat(old, new) + + class Renamer(object): """Deals with renaming of files """ @@ -1026,7 +1034,7 @@ class Renamer(object): def __init__(self, filename): self.filename = os.path.abspath(filename) - def newPath(self, new_path = None, new_fullpath = None, force = False, always_copy = False, always_move = False, leave_symlink = False, create_dirs = True, getPathPreview = False): + def newPath(self, new_path = None, new_fullpath = None, force = False, always_copy = False, prefer_hardlink_to_copy = False, always_move = False, always_hardlink = False, leave_symlink = False, create_dirs = True, getPathPreview = False): """Moves the file to a new path. If it is on the same partition, it will be moved (unless always_copy is True) @@ -1037,8 +1045,9 @@ def newPath(self, new_path = None, new_fullpath = None, force = False, always_co pointing to the file's new destination if leave_symlink is True. """ - if always_copy and always_move: - raise ValueError("Both always_copy and always_move cannot be specified") + if (always_copy, always_move, always_hardlink).count(True) > 1: + raise ValueError("Different incompatible renaming operation cannot be specified: " + "always_copy or always_move or always_hardlink") if (new_path is None and new_fullpath is None) or (new_path is not None and new_fullpath is not None): raise ValueError("Specify only new_dir or new_fullpath") @@ -1086,7 +1095,15 @@ def newPath(self, new_path = None, new_fullpath = None, force = False, always_co if always_copy: # Same partition, but forced to copy - copy_file(self.filename, new_fullpath) + if Config['prefer_hardlink_to_copy']: + try: + hardlink_file(self.filename, new_fullpath) + except OSError: + copy_file(self.filename, new_fullpath) + else: + copy_file(self.filename, new_fullpath) + elif always_hardlink: + hardlink_file(self.filename, new_fullpath) else: # Same partition, just rename the file to move it rename_file(self.filename, new_fullpath)