diff --git a/beets/library.py b/beets/library.py index 2430f71258..58f780fcd3 100644 --- a/beets/library.py +++ b/beets/library.py @@ -360,8 +360,9 @@ def remove(self): plugins.send("database_change", lib=self._db, model=self) def add(self, lib=None): + # super().add() calls self.store(), which sends `database_change`, + # so don't do it here super().add(lib) - plugins.send("database_change", lib=self._db, model=self) def __format__(self, spec): if not spec: diff --git a/beets/plugins.py b/beets/plugins.py index 299c418157..e9ec64cba9 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -25,6 +25,7 @@ import beets from beets import logging +from beets.types import EventType PLUGIN_NAMESPACE = "beetsplug" @@ -204,7 +205,7 @@ def add_media_field(self, name, descriptor): _raw_listeners = None listeners = None - def register_listener(self, event, func): + def register_listener(self, event: EventType, func): """Add a function as a listener for the specified event.""" wrapped_func = self._set_log_level_and_params(logging.WARNING, func) diff --git a/beets/types.py b/beets/types.py new file mode 100644 index 0000000000..d5fc01eec9 --- /dev/null +++ b/beets/types.py @@ -0,0 +1,33 @@ +from typing import Literal + +EventType = Literal[ + "pluginload", + "import", + "album_imported", + "album_removed", + "item_copied", + "item_imported", + "before_item_moved", + "item_moved", + "item_linked", + "item_hardlinked", + "item_reflinked", + "item_removed", + "write", + "after_write", + "import_task_created", + "import_task_start", + "import_task_apply", + "import_task_before_choice", + "import_task_choice", + "import_task_files", + "library_opened", + "database_change", + "cli_exit", + "import_begin", + "trackinfo_received", + "albuminfo_received", + "before_choose_candidate", + "mb_track_extract", + "mb_album_extract", +] diff --git a/beets/util/hidden.py b/beets/util/hidden.py index d2c66fac0f..e296e8c999 100644 --- a/beets/util/hidden.py +++ b/beets/util/hidden.py @@ -23,13 +23,15 @@ from typing import Union -def is_hidden(path: Union[bytes, Path]) -> bool: +def is_hidden(path: Union[bytes, str, Path]) -> bool: """ Determine whether the given path is treated as a 'hidden file' by the OS. """ if isinstance(path, bytes): path = Path(os.fsdecode(path)) + elif isinstance(path, str): + path = Path(path) # TODO: Avoid doing a platform check on every invocation of the function. # TODO: Stop supporting 'bytes' inputs once 'pathlib' is fully integrated. diff --git a/docs/changelog.rst b/docs/changelog.rst index 9fb3b9e3ff..69fc7bc100 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,9 @@ Bug fixes: :bug:`5265` :bug:`5371` :bug:`4715` +* Fix an issue where calling `Library.add` would cause the `database_change` + event to be sent twice, not once. + :bug:`5560` For packagers: diff --git a/test/test_database_change.py b/test/test_database_change.py new file mode 100644 index 0000000000..a67a095b71 --- /dev/null +++ b/test/test_database_change.py @@ -0,0 +1,26 @@ +import os + +import beets +import beets.logging as blog +from beets.test import _common +from beets.test.helper import BeetsTestCase, capture_log + + +class DatabaseChangeTestBase(BeetsTestCase): + def test_item_added_one_database_change(self): + self.item = _common.item() + self.item.path = beets.util.normpath( + os.path.join( + self.temp_dir, + b"a", + b"b.mp3", + ) + ) + self.item.album = "a" + self.item.title = "b" + + blog.getLogger("beets").set_global_level(blog.DEBUG) + with capture_log() as logs: + self.lib.add(self.item) + + assert logs.count("Sending event: database_change") == 1