Renaming artist/album moves the file: music folder stays organized

Editing artist/album_artist/album now relocates the file to its new
<music_folder>/Music/Artist/Album/ home (folders created as needed,
"Unknown Artist" fallback via the same organized_destination the
importer uses), deliberately relaxing the old never-move rule:

- LibraryManager.organize_root() is the shared definition of the
  managed tree; MainWindow._music_import_dir delegates to it.
- Only files already inside the organize root are managed; external
  files get their tags edited but are never pulled into the tree.
- The move rides the existing undo Command maps as a "location" field:
  Ctrl+Z moves the file back, redo re-moves, both best-effort (a failed
  physical move drops "location" so memory tracks the real file).
- A failed move applies the edit, keeps the old path, and reports via
  the new file_move_failed signal (status bar), mirroring
  tag_write_failed. Collisions get the importer's " 1" suffix.
- Directories left empty are pruned up to (never including) the root.
- conflict_resolver._merge_track_fields now carries "location" with
  the newest-date_modified rule, so a rename-move on one machine
  propagates through both the sync-conflict merge and the live
  reload_from_disk reconcile on the other.
- The on-disk basename is kept verbatim (no re-sanitizing), so files
  named on macOS aren't gratuitously renamed by a tag edit.

Same-filesystem rename keeps the player's open fd valid, so moving the
currently-playing track is safe.
This commit is contained in:
2026-07-02 16:59:59 -04:00
parent 2b02f109f6
commit 8337980d6b
4 changed files with 302 additions and 4 deletions
+6 -1
View File
@@ -110,6 +110,9 @@ class MainWindow(QMainWindow):
manager.tag_write_failed.connect(
lambda name, err: self.statusBar().showMessage(
f"Couldn't write tags to “{name}”: {err}", 8000))
manager.file_move_failed.connect(
lambda name, err: self.statusBar().showMessage(
f"Couldn't move the file for “{name}”: {err}", 8000))
manager.library_reloaded.connect(self._on_library_reloaded)
manager.conflict_resolved.connect(self._on_conflict_resolved)
prefs.changed.connect(self._on_prefs_changed)
@@ -305,7 +308,9 @@ class MainWindow(QMainWindow):
# ---- file import ----
def _music_import_dir(self) -> Path:
return Path(self._manager.library.music_folder) / "Music"
# Fall back to ./Music when no music folder is set (fresh library),
# matching the pre-organize_root behavior.
return self._manager.organize_root() or Path("Music")
def _open_files_dialog(self):
paths, _filter = QFileDialog.getOpenFileNames(