From aaea73a3918dc7fb24548866bc3064b1733f2026 Mon Sep 17 00:00:00 2001 From: iron-prog Date: Thu, 19 Mar 2026 22:09:46 +0530 Subject: [PATCH 1/2] PICARD-2496: Replace overwrite option with conflict handling strategy (skip/rename/overwrite) --- picard/file.py | 30 ++++++++++++++++++++++-------- picard/options.py | 2 +- picard/ui/options/renaming.py | 14 +++++++++++--- ui/options_renaming.ui | 22 +++++++++++++++++++--- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/picard/file.py b/picard/file.py index 1fe4eeebdf..6a8bda7490 100644 --- a/picard/file.py +++ b/picard/file.py @@ -676,8 +676,16 @@ def _rename(self, old_filename, metadata, settings): new_dirname = os.path.dirname(new_filename) if not os.path.isdir(new_dirname): os.makedirs(new_dirname) - if not settings['move_overwrite_existing_files']: - new_filename = get_available_filename(new_filename, old_filename) + config = get_config() + strategy = config.setting.get('move_conflict_strategy', 'rename') + if os.path.exists(new_filename): + if strategy == "skip": + log.warning("Destination exists, skipping move of %r", old_filename) + return old_filename + elif strategy == "rename": + new_filename = get_available_filename(new_filename, old_filename) + elif strategy == "overwrite": + pass log.debug("Moving file %r => %r", old_filename, new_filename) move_ensure_casing(old_filename, new_filename) return new_filename @@ -697,7 +705,6 @@ def _save_images(self, dirname, metadata): images = metadata.images for image in images: image.save(dirname, metadata, counters) - def _move_additional_files(self, old_filename, new_filename, config): """Move extra files, like images, playlists…""" if config.setting['move_files'] and config.setting['move_additional_files']: @@ -708,7 +715,7 @@ def _move_additional_files(self, old_filename, new_filename, config): patterns = self._compile_move_additional_files_pattern(patterns_string) try: moves = self._get_additional_files_moves(old_path, new_path, patterns) - self._apply_additional_files_moves(moves, config.setting['move_overwrite_existing_files']) + self._apply_additional_files_moves(moves) except OSError as why: log.error("Failed to scan %r: %s", old_path, why) @@ -732,15 +739,22 @@ def _get_additional_files_moves(self, old_path, new_path, patterns): yield (entry.path, new_file_path) break # we are done with this file - def _apply_additional_files_moves(self, moves, overwrite_existing_files=False): + def _apply_additional_files_moves(self, moves): + config = get_config() + strategy = config.setting.get('move_conflict_strategy', 'rename') for old_file_path, new_file_path in moves: # FIXME we shouldn't do this from a thread! if self.tagger.files.get(decode_filename(old_file_path)): log.debug("File loaded in the tagger, not moving %r", old_file_path) continue - if not overwrite_existing_files and os.path.exists(new_file_path): - log.warning("File %r already exists, not moving %r", new_file_path, old_file_path) - continue + if os.path.exists(new_file_path): + if strategy == "skip": + log.warning("File %r exists, skipping %r", new_file_path, old_file_path) + continue + elif strategy == "rename": + new_file_path = get_available_filename(new_file_path) + elif strategy == "overwrite": + pass log.debug("Moving %r to %r", old_file_path, new_file_path) try: shutil.move(old_file_path, new_file_path) diff --git a/picard/options.py b/picard/options.py index 3ecc63780d..1ceb2b517f 100644 --- a/picard/options.py +++ b/picard/options.py @@ -427,7 +427,6 @@ def make_default_toolbar_layout(): TextOption('setting', 'move_additional_files_pattern', "*.jpg *.png", title=N_("Additional file patterns")) BoolOption('setting', 'move_files', False, title=N_("Move files")) TextOption('setting', 'move_files_to', DEFAULT_MUSIC_DIR, title=N_("Destination directory")) -BoolOption('setting', 'move_overwrite_existing_files', False, title=N_("Overwrite existing files")) BoolOption('setting', 'rename_files', False, title=N_("Rename files")) # picard/ui/options/renaming_compat.py @@ -540,6 +539,7 @@ def make_default_toolbar_layout(): '', title=N_("Sessions directory"), ) +TextOption('setting', 'move_conflict_strategy', 'rename', title=N_("File conflict handling strategy")) # picard/ui/searchdialog/album.py # diff --git a/picard/ui/options/renaming.py b/picard/ui/options/renaming.py index fdd882d84b..0d711da4fa 100644 --- a/picard/ui/options/renaming.py +++ b/picard/ui/options/renaming.py @@ -75,9 +75,9 @@ class RenamingOptionsPage(OptionsPage): OPTIONS = ( ('move_files', ['move_files']), ('move_files_to', ['move_files_to']), - ('move_overwrite_existing_files', ['move_overwrite_existing_files']), ('move_additional_files', ['move_additional_files']), ('move_additional_files_pattern', ['move_additional_files_pattern']), + ('move_conflict_strategy', ['move_conflict_strategy']), ('delete_empty_dirs', ['delete_empty_dirs']), ('rename_files', ['rename_files']), ('selected_file_naming_script_id', ['naming_script_selector']), @@ -250,8 +250,11 @@ def load(self): self.ui.move_files_to.setCursorPosition(0) self.ui.move_additional_files.setChecked(config.setting['move_additional_files']) self.ui.move_additional_files_pattern.setText(config.setting['move_additional_files_pattern']) + strategy = config.setting['move_conflict_strategy'] + self.ui.move_conflict_skip.setChecked(strategy == "skip") + self.ui.move_conflict_rename.setChecked(strategy == "rename") + self.ui.move_conflict_overwrite.setChecked(strategy == "overwrite") self.ui.delete_empty_dirs.setChecked(config.setting['delete_empty_dirs']) - self.ui.move_overwrite_existing_files.setChecked(config.setting['move_overwrite_existing_files']) self.naming_scripts = config.setting['file_renaming_scripts'] self.selected_naming_script_id = config.setting['selected_file_naming_script_id'] if self.script_editor_dialog: @@ -285,9 +288,14 @@ def save(self): config.setting['move_files'] = self.ui.move_files.isChecked() config.setting['move_files_to'] = os.path.normpath(self.ui.move_files_to.text()) config.setting['move_additional_files'] = self.ui.move_additional_files.isChecked() + if self.ui.move_conflict_skip.isChecked(): + config.setting['move_conflict_strategy'] = "skip" + elif self.ui.move_conflict_rename.isChecked(): + config.setting['move_conflict_strategy'] = "rename" + elif self.ui.move_conflict_overwrite.isChecked(): + config.setting['move_conflict_strategy'] = "overwrite" config.setting['move_additional_files_pattern'] = self.ui.move_additional_files_pattern.text() config.setting['delete_empty_dirs'] = self.ui.delete_empty_dirs.isChecked() - config.setting['move_overwrite_existing_files'] = self.ui.move_overwrite_existing_files.isChecked() config.setting['selected_file_naming_script_id'] = self.selected_naming_script_id def display_error(self, error): diff --git a/ui/options_renaming.ui b/ui/options_renaming.ui index 77af9be57a..664d05d0ae 100644 --- a/ui/options_renaming.ui +++ b/ui/options_renaming.ui @@ -80,9 +80,23 @@ - + - Overwrite existing files + Do not move duplicate files + + + + + + + Move and rename file with numeric suffix + + + + + + + Move and overwrite existing files @@ -298,7 +312,9 @@ move_additional_files move_additional_files_pattern delete_empty_dirs - move_overwrite_existing_files + move_conflict_skip + move_conflict_rename + move_conflict_overwrite rename_files naming_script_selector open_script_editor From 77145b12456c6fd1993770c8a72c96a8ba2b7182 Mon Sep 17 00:00:00 2001 From: iron-prog Date: Thu, 19 Mar 2026 22:11:23 +0530 Subject: [PATCH 2/2] ruff fail --- picard/file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/file.py b/picard/file.py index 6a8bda7490..6d49ccd6cd 100644 --- a/picard/file.py +++ b/picard/file.py @@ -705,6 +705,7 @@ def _save_images(self, dirname, metadata): images = metadata.images for image in images: image.save(dirname, metadata, counters) + def _move_additional_files(self, old_filename, new_filename, config): """Move extra files, like images, playlists…""" if config.setting['move_files'] and config.setting['move_additional_files']: