diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 8c09eefea2..1da5ecde47 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -461,6 +461,12 @@ def _try_resolve_stage(stage_label: str, keep_genres, new_genres): def commands(self): lastgenre_cmd = ui.Subcommand("lastgenre", help="fetch genres") + lastgenre_cmd.parser.add_option( + "-p", + "--pretend", + action="store_true", + help="show actions but do nothing", + ) lastgenre_cmd.parser.add_option( "-f", "--force", @@ -521,45 +527,64 @@ def commands(self): def lastgenre_func(lib, opts, args): write = ui.should_write() + pretend = getattr(opts, "pretend", False) self.config.set_args(opts) if opts.album: # Fetch genres for whole albums for album in lib.albums(args): - album.genre, src = self._get_genre(album) + album_genre, src = self._get_genre(album) + prefix = "Pretend: " if pretend else "" self._log.info( - 'genre for album "{0.album}" ({1}): {0.genre}', + '{}genre for album "{.album}" ({}): {}', + prefix, album, src, + album_genre, ) - if "track" in self.sources: - album.store(inherit=False) - else: - album.store() + if not pretend: + album.genre = album_genre + if "track" in self.sources: + album.store(inherit=False) + else: + album.store() for item in album.items(): # If we're using track-level sources, also look up each # track on the album. if "track" in self.sources: - item.genre, src = self._get_genre(item) - item.store() + item_genre, src = self._get_genre(item) self._log.info( - 'genre for track "{0.title}" ({1}): {0.genre}', + '{}genre for track "{.title}" ({}): {}', + prefix, item, src, + item_genre, ) + if not pretend: + item.genre = item_genre + item.store() - if write: + if write and not pretend: item.try_write() else: # Just query singletons, i.e. items that are not part of # an album for item in lib.items(args): - item.genre, src = self._get_genre(item) - item.store() + item_genre, src = self._get_genre(item) + prefix = "Pretend: " if pretend else "" self._log.info( - "genre for track {0.title} ({1}): {0.genre}", item, src + '{}genre for track "{0.title}" ({1}): {}', + prefix, + item, + src, + item_genre, ) + if not pretend: + item.genre = item_genre + item.store() + if write and not pretend: + item.try_write() lastgenre_cmd.func = lastgenre_func return [lastgenre_cmd] diff --git a/docs/changelog.rst b/docs/changelog.rst index ba6a357b7c..63a8fe3397 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,9 @@ Unreleased New features: +- :doc:`plugins/lastgenre`: Add a ``--pretend`` option to preview genre changes + without storing or writing them. + Bug fixes: - :doc:`plugins/spotify` Fixed an issue where track matching and lookups could diff --git a/docs/plugins/lastgenre.rst b/docs/plugins/lastgenre.rst index 5ebe2d7213..230694b06c 100644 --- a/docs/plugins/lastgenre.rst +++ b/docs/plugins/lastgenre.rst @@ -124,7 +124,7 @@ tags** and will only **fetch new genres for empty tags**. When ``force`` is ``yes`` the setting of the ``whitelist`` option (as documented in Usage_) applies to any existing or newly fetched genres. -The follwing configurations are possible: +The following configurations are possible: **Setup 1** (default) @@ -213,5 +213,9 @@ fetch genres for albums or items matching a certain query. By default, ``beet lastgenre`` matches albums. To match individual tracks or singletons, use the ``-A`` switch: ``beet lastgenre -A [QUERY]``. +To preview the changes that would be made without applying them, use the ``-p`` +or ``--pretend`` flag. This shows which genres would be set but does not write +or store any changes. + To disable automatic genre fetching on import, set the ``auto`` config option to false. diff --git a/test/plugins/test_lastgenre.py b/test/plugins/test_lastgenre.py index 72b0d4f007..d6df42f977 100644 --- a/test/plugins/test_lastgenre.py +++ b/test/plugins/test_lastgenre.py @@ -14,7 +14,7 @@ """Tests for the 'lastgenre' plugin.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest @@ -131,6 +131,43 @@ def test_prefer_specific_without_canonical(self): "math rock", ] + def test_pretend_option_skips_library_updates(self): + item = self.create_item( + album="Pretend Album", + albumartist="Pretend Artist", + artist="Pretend Artist", + title="Pretend Track", + genre="Original Genre", + ) + album = self.lib.add_album([item]) + + command = self.plugin.commands()[0] + opts, args = command.parser.parse_args(["--pretend"]) + + with patch.object(lastgenre.ui, "should_write", return_value=True): + with patch.object( + self.plugin, + "_get_genre", + return_value=("Mock Genre", "mock stage"), + ) as mock_get_genre: + with patch.object(self.plugin._log, "info") as log_info: + # Mock try_write to verify it's never called in pretend mode + with patch.object(item, "try_write") as mock_try_write: + command.func(self.lib, opts, args) + + mock_get_genre.assert_called_once() + + assert any( + call.args[1] == "Pretend: " for call in log_info.call_args_list + ) + + # Verify that try_write was never called (file operations skipped) + mock_try_write.assert_not_called() + + stored_album = self.lib.get_album(album.id) + assert stored_album.genre == "Original Genre" + assert stored_album.items()[0].genre == "Original Genre" + def test_no_duplicate(self): """Remove duplicated genres.""" self._setup_config(count=99)