v0.12.1: stop asking where the music is when config.json already says

Restarting on the machine that mounts the drive at /media/trav/muzak opened
"LinTunes can't find your music folder", offering ~/Music. The library's only
music_folder is the other machine's mount point (/run/media/trav/tummult/...)
because this library predates 0.10 and has no music_folder_rel yet — but
config.json has held the right path all along under music_root, which
`--music-root … --save-config` writes and the importer stores as
library.music_folder. resolve() only looked for music_folder_override, a key
that exists solely because the prompt writes it, so a library imported the
normal way could never satisfy the check that decides whether to prompt.

music_root is now a machine-local candidate alongside music_folder_override,
consulted in the same last-resort position and subject to the same must-exist
rule, so a stale one still reports "missing" rather than resolving to a phantom
tree. Read-only: nothing writes it back, and the newer key still wins when both
are present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9ZEFi4qNJ39FMiBtiAxy2
This commit is contained in:
2026-08-25 15:06:13 -04:00
co-authored by Claude Opus 5
parent 521db2f81d
commit 7df5ecf868
4 changed files with 59 additions and 5 deletions
+4 -1
View File
@@ -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 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 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*. 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 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: 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()` `LibraryManager.set_music_folder` must call `mark_library_settings_dirty()`
+1 -1
View File
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux.""" """LinTunes — iTunes-style music library manager and player for Linux."""
__version__ = "0.12.0" __version__ = "0.12.1"
+14 -3
View File
@@ -10,7 +10,9 @@ single place that decides, in this order:
machine even when the absolute paths are not. machine even when the absolute paths are not.
2. ``music_folder`` — the legacy absolute value. Libraries written before 0.10, 2. ``music_folder`` — the legacy absolute value. Libraries written before 0.10,
and libraries last written by an older LinTunes, only have this one. 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 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 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). # Keys in the per-machine config.json (never synced).
OVERRIDE_KEY = "music_folder_override" OVERRIDE_KEY = "music_folder_override"
PROMPT_SEEN_KEY = "music_folder_prompted" 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) @dataclass(frozen=True)
@@ -73,8 +82,10 @@ def resolve(library, data_dir, config=None) -> Resolution:
if found is not None: if found is not None:
return Resolution(found, "legacy", legacy) return Resolution(found, "legacy", legacy)
override = config.get(OVERRIDE_KEY, "") for key in (OVERRIDE_KEY, LEGACY_OVERRIDE_KEY):
if override: override = config.get(key, "")
if not override:
continue
found = _existing_dir(override) found = _existing_dir(override)
if found is not None: if found is not None:
return Resolution(found, "machine", override) return Resolution(found, "machine", override)
+40
View File
@@ -241,6 +241,46 @@ class TestMusicFolderResolution:
assert state.path == elsewhere assert state.path == elsewhere
assert state.source == "machine" 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, def test_unset_and_missing_are_different_conversations(self, tmp_path,
isolated_config): isolated_config):
assert music_folder.resolve(Library(), tmp_path).source == "unset" assert music_folder.resolve(Library(), tmp_path).source == "unset"