From 0696cdcdf459c57017259e18d3dec9f96714ec5a Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sat, 18 Apr 2026 17:26:26 +0200 Subject: [PATCH 1/2] Parallelize candidate lookup for metadata plugins. --- beets/metadata_plugins.py | 29 +++++++++++++++++++++++++---- docs/changelog.rst | 3 +++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py index 9e5e2081e2..0cecd1dbee 100644 --- a/beets/metadata_plugins.py +++ b/beets/metadata_plugins.py @@ -9,6 +9,7 @@ import abc import re +from concurrent.futures import ThreadPoolExecutor, as_completed from contextlib import contextmanager from functools import cache, cached_property, wraps from typing import ( @@ -79,10 +80,30 @@ def _yield_from_plugins( @wraps(func) def wrapper(*args, **kwargs) -> Iterator[Ret]: - for plugin in find_metadata_source_plugins(): - method = getattr(plugin, method_name) - with maybe_handle_plugin_error(plugin, method_name): - yield from filter(None, method(*args, **kwargs)) + # Run all metadata source plugins in parallel threads. + # Each plugin executes its lookup/search concurrently, so I/O-bound API calls + # complete faster than sequential. + with ThreadPoolExecutor() as executor: + futures = { + executor.submit( + # Evaluate iterator with list such that results are ready when + # future.result() is called. + lambda *args, **kwargs: list( + getattr(plugin, method_name)( + *args, + **kwargs, + ) + ), + *args, + **kwargs, + ): plugin + for plugin in find_metadata_source_plugins() + } + + for future in as_completed(futures): + plugin = futures[future] + with maybe_handle_plugin_error(plugin, method_name): + yield from filter(None, future.result()) return wrapper diff --git a/docs/changelog.rst b/docs/changelog.rst index eec6c645c2..3a5fe6d9b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,9 @@ New features CLI flag to skip re-fetching lyrics for tracks that already have synced lyrics, even when ``force`` is enabled. :bug:`5249` - :doc:`plugins/musicbrainz`: Use aliases for artist credit. +- Execute metadata source plugin searches and lookups concurrently, reducing + total wait time when multiple plugins (e.g. MusicBrainz and Spotify) are + enabled. Bug fixes ~~~~~~~~~ From f7ed69bf292e842e467cb6374c4f13bc1a9a4b24 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sat, 18 Apr 2026 19:38:56 +0200 Subject: [PATCH 2/2] Minified comments and materialize iterators --- beets/metadata_plugins.py | 4 +--- docs/changelog.rst | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py index 0cecd1dbee..e348b7a6d3 100644 --- a/beets/metadata_plugins.py +++ b/beets/metadata_plugins.py @@ -80,9 +80,7 @@ def _yield_from_plugins( @wraps(func) def wrapper(*args, **kwargs) -> Iterator[Ret]: - # Run all metadata source plugins in parallel threads. - # Each plugin executes its lookup/search concurrently, so I/O-bound API calls - # complete faster than sequential. + # Run plugin methods concurrently for faster I/O-bound lookups. with ThreadPoolExecutor() as executor: futures = { executor.submit( diff --git a/docs/changelog.rst b/docs/changelog.rst index 3a5fe6d9b9..170481574b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,8 +35,8 @@ New features CLI flag to skip re-fetching lyrics for tracks that already have synced lyrics, even when ``force`` is enabled. :bug:`5249` - :doc:`plugins/musicbrainz`: Use aliases for artist credit. -- Execute metadata source plugin searches and lookups concurrently, reducing - total wait time when multiple plugins (e.g. MusicBrainz and Spotify) are +- Metadata source plugin searches and lookups are now executed concurrently, + speeding up lookups when multiple plugins (e.g. MusicBrainz and Spotify) are enabled. Bug fixes