diff --git a/CLAUDE.md b/CLAUDE.md index 263f76b..db2be07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,10 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal floor between versions), `music_folder_rel` relative to the data dir (the portable one, same trick as `Track.location`), and `music_folder_set_at`, a stamp so a merge prefers the newest *setting* rather than the newest *file*. - `resolve()` tries rel → legacy → the per-machine override in `config.json`, + `resolve()` tries rel → legacy → the per-machine override in `config.json` + (both `music_folder_override` *and* the older `music_root` that + `--music-root … --save-config` writes — an imported library already + knows where its music is on this machine, so don't ask), and every candidate must **exist** — the override is consulted last so a machine that once needed one isn't pinned to it forever. Two rules that bite: `LibraryManager.set_music_folder` must call `mark_library_settings_dirty()` diff --git a/lintunes/__init__.py b/lintunes/__init__.py index cdecade..ca5e082 100644 --- a/lintunes/__init__.py +++ b/lintunes/__init__.py @@ -1,3 +1,3 @@ """LinTunes — iTunes-style music library manager and player for Linux.""" -__version__ = "0.12.0" +__version__ = "0.12.1" diff --git a/lintunes/music_folder.py b/lintunes/music_folder.py index b176c77..c0127fc 100644 --- a/lintunes/music_folder.py +++ b/lintunes/music_folder.py @@ -10,7 +10,9 @@ single place that decides, in this order: machine even when the absolute paths are not. 2. ``music_folder`` — the legacy absolute value. Libraries written before 0.10, and libraries last written by an older LinTunes, only have this one. -3. The machine-local override in ``~/.config/lintunes/config.json``. +3. The machine-local override in ``~/.config/lintunes/config.json`` — either + ``music_folder_override`` (set by the folder prompt) or the older + ``music_root`` that ``--music-root … --save-config`` has always written. The override is consulted **last, not first**, on purpose. If it won outright, a machine that once needed an override would be pinned to it forever and would @@ -32,6 +34,13 @@ from lintunes.paths import to_absolute # Keys in the per-machine config.json (never synced). OVERRIDE_KEY = "music_folder_override" PROMPT_SEEN_KEY = "music_folder_prompted" +# The same fact under its pre-0.10 name: `--music-root … --save-config` has +# always written this machine's media folder here, and the importer stores it as +# `library.music_folder`. Every library set up that way therefore has the right +# answer sitting in config.json, and ignoring it meant a machine that mounts the +# drive somewhere else was asked to locate a folder it had already been told +# about. Read-only — nothing writes it back, so the newer key stays canonical. +LEGACY_OVERRIDE_KEY = "music_root" @dataclass(frozen=True) @@ -73,8 +82,10 @@ def resolve(library, data_dir, config=None) -> Resolution: if found is not None: return Resolution(found, "legacy", legacy) - override = config.get(OVERRIDE_KEY, "") - if override: + for key in (OVERRIDE_KEY, LEGACY_OVERRIDE_KEY): + override = config.get(key, "") + if not override: + continue found = _existing_dir(override) if found is not None: return Resolution(found, "machine", override) diff --git a/tests/test_round40.py b/tests/test_round40.py index f4e37a6..27a6c5d 100644 --- a/tests/test_round40.py +++ b/tests/test_round40.py @@ -241,6 +241,46 @@ class TestMusicFolderResolution: assert state.path == elsewhere assert state.source == "machine" + def test_the_pre_010_music_root_key_counts_as_an_override( + self, tmp_path, isolated_config): + """`--music-root … --save-config` has always written this machine's + media folder to config.json as `music_root`. Every library imported that + way already knows where its music is here, so a machine that mounts the + drive at a different path must not be asked to go find it.""" + from lintunes.config import save_config + media = tmp_path / "media" + media.mkdir() + save_config({"music_root": str(media)}) + + # The shared value is another machine's mount point, unreachable here. + gone = Library(music_folder="/run/media/trav/tummult/music/iTunes Media") + state = music_folder.resolve(gone, tmp_path) + + assert state.path == media + assert state.source == "machine" + + def test_the_newer_override_key_still_wins(self, tmp_path, isolated_config): + from lintunes.config import save_config + chosen, imported = tmp_path / "chosen", tmp_path / "imported" + chosen.mkdir() + imported.mkdir() + save_config({"music_folder_override": str(chosen), + "music_root": str(imported)}) + + state = music_folder.resolve(Library(music_folder="/nope"), tmp_path) + + assert state.path == chosen + + def test_a_stale_music_root_does_not_resolve(self, tmp_path, isolated_config): + """It's still a candidate, not an answer — the folder has to be there.""" + from lintunes.config import save_config + save_config({"music_root": str(tmp_path / "unplugged")}) + + state = music_folder.resolve(Library(music_folder="/nope"), tmp_path) + + assert state.path is None + assert state.source == "missing" + def test_unset_and_missing_are_different_conversations(self, tmp_path, isolated_config): assert music_folder.resolve(Library(), tmp_path).source == "unset"