From 7a1c92d8617afee535ec39eb7b201226e24deb79 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 19 Jan 2026 21:34:02 -0800 Subject: [PATCH] Adjust log message. Initial fix for group albums. Test outlines written. TODO: Write tests. --- beetsplug/fromfilename.py | 22 ++++++++++++++++--- docs/plugins/fromfilename.rst | 11 ++++++++++ test/plugins/test_fromfilename.py | 36 ++++++++++++++++++++++++++++--- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 9960dfe97..82c3d444c 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -164,6 +164,7 @@ class FromFilenamePlugin(BeetsPlugin): ], "patterns": {"folder": [], "file": []}, "ignore_dirs": [], + "guess": {"folder": True, "file": True}, } ) self.fields = set(self.config["fields"].as_str_seq()) @@ -341,10 +342,17 @@ class FromFilenamePlugin(BeetsPlugin): return trackmatch def _parse_album_info(self, text: str) -> FilenameMatch: + matches = FilenameMatch() + + if not self.config["guess"]["folder"] or ( + config["import"]["group_albums"] or config["import"]["singletons"] + ): + # If the group albums flag is thrown, we can't trust the parent directory + # likewise for singletons - return an empty match + return matches # Check if a user pattern matches if m := self._check_user_matches(text, self.folder_patterns): return m - matches = FilenameMatch() # Start with the extra fields to make parsing # the album artist and artist field easier year, span = self._parse_year(text) @@ -388,16 +396,24 @@ class FromFilenamePlugin(BeetsPlugin): for item in track_matches: match.update(track_matches[item]._matches) found_data: dict[str, int | str] = {} - self._log.debug(f"Attempting keys: {match.keys()}") + self._log.debug(f"keys: {', '.join(match.keys())}") + # Check every key we are supposed to match. for key in match.keys(): + # If the key is applicable to the session, we will update it. if key in self.session_fields: old_value = item.get(key) new_value = match[key] + # If the field is bad, and we have a new value if self._bad_field(old_value) and new_value: found_data[key] = new_value - self._log.info(f"Item updated with: {found_data.items()}") + self._log.info(f"guessing {self._format_guesses(found_data)}") item.update(found_data) + @staticmethod + def _format_guesses(guesses: dict[str, int | str]) -> str: + """Format guesses in a 'field="guess"' style for logging""" + return ", ".join([f'{g[0]}="{g[1]}"' for g in guesses.items()]) + @staticmethod def _parse_album_and_albumartist( text: str, diff --git a/docs/plugins/fromfilename.rst b/docs/plugins/fromfilename.rst index c4f15a59e..1c48caffb 100644 --- a/docs/plugins/fromfilename.rst +++ b/docs/plugins/fromfilename.rst @@ -84,3 +84,14 @@ Default Specify parent directory names that will not be searched for album information. Useful if you use a regular directory for importing single files. + +.. conf:: guess + + Disable guessing from the folder or filename. Be aware that disabling both + will cause the plugin to have no effect! + + .. code-block:: yaml + + guess: + folder: yes + file: yes diff --git a/test/plugins/test_fromfilename.py b/test/plugins/test_fromfilename.py index 7d264e396..cf444993d 100644 --- a/test/plugins/test_fromfilename.py +++ b/test/plugins/test_fromfilename.py @@ -735,7 +735,7 @@ class TestFromFilename(PluginMixin): ], ) def test_alphanumeric_index(self, expected): - """Test parsing an alphanumeric index string.""" + """Assert that an alphanumeric index is guessed in order.""" task = mock_task([mock_item(path=item.path) for item in expected]) f = FromFilenamePlugin() f.filename_task(task, Session()) @@ -743,7 +743,9 @@ class TestFromFilename(PluginMixin): assert task.items[1].track == expected[1].track assert task.items[2].track == expected[2].track - def test_no_changes(self): + def test_no_guesses(self): + """Assert that an item with complete information is + has no guesses attempted.""" item = mock_item( path="/Folder/File.wav", albumartist="AlbumArtist", @@ -758,7 +760,9 @@ class TestFromFilename(PluginMixin): f.filename_task(task, Session()) mock.assert_not_called() - def test_changes_missing_values(self): + def test_only_one_guess(self): + """Assert that an item missing only one value + will just have that key in session fields.""" item = mock_item( path="/Folder/File.wav", albumartist="AlbumArtist", @@ -782,9 +786,35 @@ class TestFromFilename(PluginMixin): mock.assert_called() def test_ignored_directories(self): + """Assert that a given parent directory name is ignored.""" ignored = "Incoming" item = mock_item(path="/tmp/" + ignored + "/01 - File.wav") with self.configure_plugin({"ignore_dirs": [ignored]}): f = FromFilenamePlugin() parent_folder, _ = f._get_path_strings([item]) assert parent_folder == "" + + def test_guess_folder(self): + """Assert that from filename does not + guess from the folder, if guess folder is `no`.""" + return + + def test_guess_file(self): + """Assert that from filename does not guess + from the file, if guess file is `no`.""" + return + + def test_singleton_flag_import(self): + """If the import task is a singleton, assert that + the plugin does not guess from the folder.""" + return + + def test_group_album_flag_import(self): + """If the group albums flag is thrown, assert + that the plugin does not guess from the folder.""" + return + + def test_import_split_by_group(self): + """Asser that an initial run without group by album, and an inaccurate + album guess, results in a run omitting it with the group album flag.""" + return