Split up data and cache with XDG directories (#2304)
authorJamie Gravendeel <me@jamie.garden>
Tue, 12 Aug 2025 12:09:53 +0000 (14:09 +0200)
committerGitHub <noreply@github.com>
Tue, 12 Aug 2025 12:09:53 +0000 (14:09 +0200)
music_assistant/__main__.py
music_assistant/mass.py
tests/conftest.py

index a2b2e3e7139209b780fff09f441faeea41f88122..50feb951477ecab5cefa867d78cfa4f8c6ff5e07 100644 (file)
@@ -35,17 +35,32 @@ def get_arguments() -> argparse.Namespace:
     """Arguments handling."""
     parser = argparse.ArgumentParser(description="MusicAssistant")
 
-    default_data_dir = os.getenv("APPDATA") if os.name == "nt" else os.path.expanduser("~")
-    if not default_data_dir:
-        parser.error("Unable to find default data dir")
-    default_data_dir = os.path.join(default_data_dir, ".musicassistant")
+    if os.path.isdir(old_data_dir := os.path.join(os.path.expanduser("~"), ".musicassistant")):
+        data_dir = old_data_dir
+        cache_dir = os.path.join(data_dir, ".cache")
+    else:
+        data_dir = os.path.join(
+            os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")),
+            "music-assistant",
+        )
+        cache_dir = os.path.join(
+            os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
+            "music-assistant",
+        )
 
     parser.add_argument(
+        "--data-dir",
         "-c",
         "--config",
-        metavar="path_to_config_dir",
-        default=default_data_dir,
-        help="Directory that contains the MusicAssistant configuration",
+        metavar="path_to_data_dir",
+        default=data_dir,
+        help="Directory that contains MusicAssistant persistent data",
+    )
+    parser.add_argument(
+        "--cache-dir",
+        metavar="path_to_cache_dir",
+        default=cache_dir,
+        help="Directory that contains MusicAssistant cache data",
     )
     parser.add_argument(
         "--log-level",
@@ -59,6 +74,7 @@ def get_arguments() -> argparse.Namespace:
         action=argparse.BooleanOptionalAction,
         help="Start in safe mode (core controllers only, no providers)",
     )
+
     return parser.parse_args()
 
 
@@ -174,9 +190,11 @@ def main() -> None:
     """Start MusicAssistant."""
     # parse arguments
     args = get_arguments()
-    data_dir = args.config
-    if not os.path.isdir(data_dir):
-        os.makedirs(data_dir)
+
+    data_dir = args.data_dir
+    cache_dir = args.cache_dir
+
+    os.makedirs(data_dir, exist_ok=True)
 
     # TEMP: override options though hass config file
     hass_options_file = os.path.join(data_dir, "options.json")
@@ -195,7 +213,7 @@ def main() -> None:
 
     # setup logger
     logger = setup_logger(data_dir, log_level)
-    mass = MusicAssistant(data_dir, safe_mode)
+    mass = MusicAssistant(data_dir, cache_dir, safe_mode)
 
     # enable alpine subprocess workaround
     _enable_posix_spawn()
index 53a36ab2799b2265a715a55984e4f1c38cb8ad5b..049d222ae0a78ddcd2c67293468f27bd6ddd0ba3 100644 (file)
@@ -112,10 +112,10 @@ class MusicAssistant:
     streams: StreamsController
     _aiobrowser: AsyncServiceBrowser
 
-    def __init__(self, storage_path: str, safe_mode: bool = False) -> None:
+    def __init__(self, storage_path: str, cache_path: str, safe_mode: bool = False) -> None:
         """Initialize the MusicAssistant Server."""
         self.storage_path = storage_path
-        self.cache_path = os.path.join(storage_path, ".cache")
+        self.cache_path = cache_path
         self.safe_mode = safe_mode
         # we dynamically register command handlers which can be consumed by the apis
         self.command_handlers: dict[str, APICommandHandler] = {}
index 2dc3e1f63934f12c783fbe23924a8fc3e6068ba9..ff92acfbcf4c0d80ce14304262891f9faac61c66 100644 (file)
@@ -19,12 +19,14 @@ def caplog_fixture(caplog: pytest.LogCaptureFixture) -> pytest.LogCaptureFixture
 @pytest.fixture
 async def mass(tmp_path: pathlib.Path) -> AsyncGenerator[MusicAssistant, None]:
     """Start a Music Assistant in test mode."""
-    storage_path = tmp_path / "root"
+    storage_path = tmp_path / "data"
+    cache_path = tmp_path / "cache"
     storage_path.mkdir(parents=True)
+    cache_path.mkdir(parents=True)
 
     logging.getLogger("aiosqlite").level = logging.INFO
 
-    mass = MusicAssistant(str(storage_path))
+    mass = MusicAssistant(str(storage_path), str(cache_path))
 
     await mass.start()