From a57ef2cb3b037db43277595aaff974f1120baa15 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 14 Sep 2025 15:36:42 -0400 Subject: [PATCH 1/9] Add --pretend option to lastgenre plugin for previewing genre changes --- beetsplug/lastgenre/__init__.py | 44 +++++++++++++++++++++++---------- docs/changelog.rst | 3 +++ docs/plugins/lastgenre.rst | 4 +++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 8c09eefea..bebe15047 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -461,6 +461,12 @@ class LastGenrePlugin(plugins.BeetsPlugin): 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,57 @@ class LastGenrePlugin(plugins.BeetsPlugin): 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) self._log.info( - 'genre for album "{0.album}" ({1}): {0.genre}', + 'genre for album "{0.album}" ({1}): {}', 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 "{0.title}" ({1}): {}', 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) self._log.info( - "genre for track {0.title} ({1}): {0.genre}", item, src + 'genre for track "{0.title}" ({1}): {}', + item, + src, + item_genre, ) + if not pretend: + item.genre = item_genre + item.store() lastgenre_cmd.func = lastgenre_func return [lastgenre_cmd] diff --git a/docs/changelog.rst b/docs/changelog.rst index ba6a357b7..63a8fe339 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 5ebe2d721..f4f92f7d1 100644 --- a/docs/plugins/lastgenre.rst +++ b/docs/plugins/lastgenre.rst @@ -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 changes without modifying your library, use the ``-p`` +(``--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. From 84986dc42d315c52ca32b130862fb16d762c0237 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 14 Sep 2025 15:49:34 -0400 Subject: [PATCH 2/9] Enhance lastgenre plugin: add item.try_write() for write operations and improve documentation clarity --- beetsplug/lastgenre/__init__.py | 2 ++ docs/plugins/lastgenre.rst | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index bebe15047..cfecbf0f8 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -578,6 +578,8 @@ class LastGenrePlugin(plugins.BeetsPlugin): 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/plugins/lastgenre.rst b/docs/plugins/lastgenre.rst index f4f92f7d1..a33a79230 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,9 +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 changes without modifying your library, use the ``-p`` -(``--pretend``) flag. This shows which genres would be set but does not write -or store any changes. +- To preview the changes that would be made without applying them, use the + ``-p`` (``--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. From 95b35ded4ab74234f8de470e57217b87d36b920b Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 14 Sep 2025 15:54:12 -0400 Subject: [PATCH 3/9] Lint --- docs/plugins/lastgenre.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/lastgenre.rst b/docs/plugins/lastgenre.rst index a33a79230..a932585ac 100644 --- a/docs/plugins/lastgenre.rst +++ b/docs/plugins/lastgenre.rst @@ -213,9 +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`` (``--pretend``) flag. This shows which genres would be set but does - not write or store any changes. +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. From 56e132f3527b5abca61dec676bf43e78b5a9a607 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 14 Sep 2025 16:00:01 -0400 Subject: [PATCH 4/9] more lint --- docs/plugins/lastgenre.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/lastgenre.rst b/docs/plugins/lastgenre.rst index a932585ac..230694b06 100644 --- a/docs/plugins/lastgenre.rst +++ b/docs/plugins/lastgenre.rst @@ -213,9 +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 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. From 5e6dd674a965c89c4fa9c18f5d99b7d9df9c39b6 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Wed, 17 Sep 2025 07:47:04 -0400 Subject: [PATCH 5/9] Update beetsplug/lastgenre/__init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šarūnas Nejus --- beetsplug/lastgenre/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index cfecbf0f8..fc8510a0a 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -535,7 +535,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): for album in lib.albums(args): album_genre, src = self._get_genre(album) self._log.info( - 'genre for album "{0.album}" ({1}): {}', + 'genre for album "{.album}" ({}): {}', album, src, album_genre, From 0be4cecf820785633feac956ccadca4b8da3f5a8 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Wed, 17 Sep 2025 07:47:14 -0400 Subject: [PATCH 6/9] Update beetsplug/lastgenre/__init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šarūnas Nejus --- beetsplug/lastgenre/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index fc8510a0a..946b041c3 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -553,7 +553,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): if "track" in self.sources: item_genre, src = self._get_genre(item) self._log.info( - 'genre for track "{0.title}" ({1}): {}', + 'genre for track "{.title}" ({}): {}', item, src, item_genre, From c7ee0e326c3579e6a8207567621b781f2f60a530 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 18 Sep 2025 07:53:48 -0400 Subject: [PATCH 7/9] Add prefix to log messages for genre fetching in LastGenrePlugin --- beetsplug/lastgenre/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 946b041c3..1da5ecde4 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -534,8 +534,10 @@ class LastGenrePlugin(plugins.BeetsPlugin): # Fetch genres for whole albums for album in lib.albums(args): album_genre, src = self._get_genre(album) + prefix = "Pretend: " if pretend else "" self._log.info( - 'genre for album "{.album}" ({}): {}', + '{}genre for album "{.album}" ({}): {}', + prefix, album, src, album_genre, @@ -553,7 +555,8 @@ class LastGenrePlugin(plugins.BeetsPlugin): if "track" in self.sources: item_genre, src = self._get_genre(item) self._log.info( - 'genre for track "{.title}" ({}): {}', + '{}genre for track "{.title}" ({}): {}', + prefix, item, src, item_genre, @@ -569,8 +572,10 @@ class LastGenrePlugin(plugins.BeetsPlugin): # an album for item in lib.items(args): item_genre, src = self._get_genre(item) + prefix = "Pretend: " if pretend else "" self._log.info( - 'genre for track "{0.title}" ({1}): {}', + '{}genre for track "{0.title}" ({1}): {}', + prefix, item, src, item_genre, From 9b1537f22625746019f102e7f057001e957e5689 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 18 Sep 2025 08:20:40 -0400 Subject: [PATCH 8/9] Add test for --pretend option in LastGenrePlugin to skip library updates --- test/plugins/test_lastgenre.py | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/test/plugins/test_lastgenre.py b/test/plugins/test_lastgenre.py index 72b0d4f00..273be4b28 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,41 @@ class LastGenrePluginTest(BeetsTestCase): "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="", + ) + 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: + 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 + ) + + stored_album = self.lib.get_album(album.id) + assert stored_album.genre == "" + + items = list(stored_album.items()) + assert items + assert items[0].genre == "" + def test_no_duplicate(self): """Remove duplicated genres.""" self._setup_config(count=99) From 2e307b519af55b6e6a12eb9f9bcb72d13f8ac5cc Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sat, 20 Sep 2025 19:18:56 +0200 Subject: [PATCH 9/9] lastgenre: Also mock try_write in test_pretend.. and add and original genre instead empty string (clarify intention of test / readability). Remove not really necessary assert items checks. --- test/plugins/test_lastgenre.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/plugins/test_lastgenre.py b/test/plugins/test_lastgenre.py index 273be4b28..d6df42f97 100644 --- a/test/plugins/test_lastgenre.py +++ b/test/plugins/test_lastgenre.py @@ -137,7 +137,7 @@ class LastGenrePluginTest(BeetsTestCase): albumartist="Pretend Artist", artist="Pretend Artist", title="Pretend Track", - genre="", + genre="Original Genre", ) album = self.lib.add_album([item]) @@ -151,7 +151,9 @@ class LastGenrePluginTest(BeetsTestCase): return_value=("Mock Genre", "mock stage"), ) as mock_get_genre: with patch.object(self.plugin._log, "info") as log_info: - command.func(self.lib, opts, args) + # 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() @@ -159,12 +161,12 @@ class LastGenrePluginTest(BeetsTestCase): call.args[1] == "Pretend: " for call in log_info.call_args_list ) - stored_album = self.lib.get_album(album.id) - assert stored_album.genre == "" + # Verify that try_write was never called (file operations skipped) + mock_try_write.assert_not_called() - items = list(stored_album.items()) - assert items - assert items[0].genre == "" + 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."""