From bf922dfe7e2ad3e4731d0f44047c89fabf31e208 Mon Sep 17 00:00:00 2001 From: Ava <170544612+Ava-91@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:08:11 +0330 Subject: [PATCH 1/2] Validate filename template placeholders in profiles --- src/luna/config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/luna/config.py b/src/luna/config.py index cc86339..4a9c820 100644 --- a/src/luna/config.py +++ b/src/luna/config.py @@ -1,9 +1,11 @@ from dataclasses import dataclass, asdict from pathlib import Path +from string import Formatter import json DEFAULT_EXTENSIONS = (".mp3", ".flac", ".m4a", ".aac", ".ogg", ".opus", ".wav", ".wma") _CONFIG_FIELDS = {"roots", "extensions", "ignored_paths", "max_workers", "filename_template"} +_SUPPORTED_FILENAME_FIELDS = {"track", "artist", "title", "album", "album_artist", "disc", "year", "genre", "format"} @dataclass @@ -19,6 +21,20 @@ def defaults(cls): return cls([], list(DEFAULT_EXTENSIONS), []) +def _validate_filename_template(template: str) -> None: + formatter = Formatter() + try: + parts = list(formatter.parse(template)) + except ValueError as exc: + raise ValueError(f"Configuration 'filename_template' is invalid: {exc}") from exc + + for _, field_name, format_spec, _ in parts: + if field_name is not None and field_name not in _SUPPORTED_FILENAME_FIELDS: + raise ValueError(f"Configuration 'filename_template' uses unsupported field: {field_name}") + if format_spec: + _validate_filename_template(format_spec) + + def validate_profile(profile: LibraryProfile) -> LibraryProfile: if not isinstance(profile.roots, list) or not all(isinstance(value, str) and value.strip() for value in profile.roots): raise ValueError("Configuration 'roots' must be a list of non-empty strings.") @@ -30,6 +46,7 @@ def validate_profile(profile: LibraryProfile) -> LibraryProfile: raise ValueError("Configuration 'max_workers' must be a positive integer.") if not isinstance(profile.filename_template, str) or not profile.filename_template.strip(): raise ValueError("Configuration 'filename_template' must be a non-empty string.") + _validate_filename_template(profile.filename_template) return profile From 84171e01ce67202f32b5e58fa68a23c914f522a1 Mon Sep 17 00:00:00 2001 From: Ava <170544612+Ava-91@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:08:22 +0330 Subject: [PATCH 2/2] Test supported and unsupported filename template fields --- tests/test_config.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index beed962..4ea39ff 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -30,6 +30,22 @@ def test_invalid_filename_template_is_rejected(self): with self.assertRaisesRegex(ValueError, "filename_template.*non-empty string"): validate_profile(LibraryProfile([], [".mp3"], [], filename_template="")) + def test_all_supported_filename_placeholders_are_valid(self): + template = "{track} {artist} {title} {album} {album_artist} {disc} {year} {genre} {format}" + self.assertEqual(validate_profile(LibraryProfile([], [".mp3"], [], filename_template=template)).filename_template, template) + + def test_unsupported_filename_placeholder_is_rejected(self): + with self.assertRaisesRegex(ValueError, "filename_template.*unsupported field.*banana"): + validate_profile(LibraryProfile([], [".mp3"], [], filename_template="{banana}")) + + def test_malformed_filename_template_is_rejected(self): + with self.assertRaisesRegex(ValueError, "filename_template.*invalid"): + validate_profile(LibraryProfile([], [".mp3"], [], filename_template="{title")) + + def test_nested_unsupported_filename_placeholder_is_rejected(self): + with self.assertRaisesRegex(ValueError, "filename_template.*unsupported field.*width"): + validate_profile(LibraryProfile([], [".mp3"], [], filename_template="{title:{width}}")) + def test_unknown_configuration_fields_are_rejected(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "config.json"