Skip to content

Conversation

nicomem
Copy link
Contributor

@nicomem nicomem commented Aug 30, 2025

Description

General information

This PR continues the work started on #4936 by @Louson by rebasing the changes on master, fixing errors encountered, and also converting all usages of musicbrainzngs in the other plugins to mbzero.

How this PR is composed

This PR is composed of the following commits:

  • The first commit does the preliminary work of defining an MbInterface usable by plugins and migrates the musicbrainz plugin to mbzero
    • These changes started from the changes of the original PR, and applied some fixes and modifications to be able to work correctly and easily across multiple beets plugins
  • The following commits each migrate one beet plugin to mbzero
    • The last one also removing the musicbrainzngs library since it is not used anymore
  • Finally, a commit of documentation to explicit and explain the configuration options shared between all plugins doing MusicBrainz requests

Each commit comes with a description detailling useful information about them.

Other benefits

On top of migrating off of musicbrainzngs which does not receive any update anymore, this PR already solves some problems I found while developping it:

  • There is a bug in the musicbrainzngs rate-limiter and it does not work correctly for non-default configuration (see Invalid rate limiting sleeping due to error in computation alastair/python-musicbrainzngs#294 for more information). The new implementation does not suffer of this problem, and has quite extensive testing added to make sure of its correctness.
  • The musicbrainz configuration options were previously only read and applied by the musicbrainz plugin; meaning that if it were disabled, the other plugins making MusicBrainz requests did not use the configuration. The shared configuration options is now applied by SharedMbInterface which is thus applied when the first plugin making use of it is initialized.

Tests done

Tested with poe test, which returns no failure, and also by enabling integration tests for the parentwork plugin tests.

I also manually tested a lot the musicbrainz plugin by importing my quite varied music library (e.g. JP artists and titles, very long titles, missing tracks in albums, etc.), and tested many different user inputs (e.g. giving invalid MBID, aborting and continuing the import, importing as tracks or albums, etc.).
I also tried to test manually the other plugins as best as I could, but since I do not use them much, there may be cases which I may have not tested (but I did make sure to test all of them for at least a few requests).

To Do

  • Documentation. (If you've added a new command-line flag, for example, find the appropriate page under docs/ to describe it.)
  • Changelog. (Add an entry to docs/changelog.rst to the bottom of one of the lists near the top of the document.)
  • Tests. (Very much encouraged but not strictly required.)

Summary by Sourcery

Replace all direct uses of the legacy musicbrainzngs library with a new mbzero-based MbInterface abstraction and consolidate MusicBrainz configuration and rate limiting across plugins

New Features:

  • Add MbInterface and SharedMbInterface layer built on mbzero for all MusicBrainz API interactions
  • Introduce a thread-safe RateLimiter utility to control and share API request rates

Bug Fixes:

  • Resolve incorrect rate-limiting behavior found in musicbrainzngs by using the new RateLimiter implementation

Enhancements:

  • Migrate MusicBrainz, ParentWork, MBCollection, ListenBrainz, Missing, and related tests to use the new interface and normalized JSON field names
  • Consolidate and centralize MusicBrainz configuration options in SharedMbInterface for consistent behavior across plugins
  • Remove the direct musicbrainzngs dependency and update project dependencies to include mbzero

Build:

  • Update pyproject.toml to drop musicbrainzngs and add mbzero dependency

Documentation:

  • Document the shared MusicBrainz configuration options for plugins in updated plugin documentation

Tests:

  • Add unit tests for RateLimiter covering single-threaded and multi-threaded scenarios

Copy link
Contributor

sourcery-ai bot commented Aug 30, 2025

Reviewer's Guide

This PR refactors all MusicBrainz interactions to use a new mbzero-based interface (MbInterface/SharedMbInterface), centralizes configuration and rate limiting, migrates existing plugins off musicbrainzngs (updating error handling and JSON key conventions), removes the direct musicbrainzngs dependency in favor of mbzero, and adds a thread-safe RateLimiter utility with comprehensive tests.

Sequence diagram for MusicBrainz API request with shared rate limiting

sequenceDiagram
    participant Plugin
    participant SharedMbInterface
    participant MbInterface
    participant RateLimiter
    participant mbzero
    Plugin->>SharedMbInterface: get()
    SharedMbInterface->>MbInterface: return instance
    Plugin->>MbInterface: API request (e.g. get_release_by_id)
    MbInterface->>RateLimiter: __enter__ (acquire rate limit)
    RateLimiter-->>MbInterface: allow request
    MbInterface->>mbzero: send request
    mbzero-->>MbInterface: response
    MbInterface->>Plugin: return result
Loading

ER diagram for MusicBrainz configuration centralization

erDiagram
    MUSICBRAINZ_CONFIG {
        host string
        https bool
        ratelimit int
        ratelimit_interval int
        user string
        pass string
    }
    SHARED_MB_INTERFACE {
        uses MUSICBRAINZ_CONFIG
    }
    PLUGIN {
        uses SHARED_MB_INTERFACE
    }
    SHARED_MB_INTERFACE ||--o| MUSICBRAINZ_CONFIG: centralizes
    PLUGIN ||--o| SHARED_MB_INTERFACE: accesses
Loading

Class diagram for new MbInterface and SharedMbInterface

classDiagram
    class MbInterface {
        -hostname: str
        -https: bool
        -rate_limiter: RateLimiter
        -useragent: str
        -auth: MbzCredentials | None
        +browse_recordings(...)
        +browse_release(...)
        +browse_release_groups(...)
        +get_release_by_id(...)
        +get_recording_by_id(...)
        +get_work_by_id(...)
        +get_user_collections(...)
        +search_releases(...)
        +search_recordings(...)
        +add_releases_to_collection(...)
        +remove_releases_from_collection(...)
    }
    class SharedMbInterface {
        -mb_interface: MbInterface
        +require_auth_for_plugin(...)
        +get(): MbInterface
    }
    SharedMbInterface --> MbInterface
    class RateLimiter {
        -reqs_per_interval: int
        -interval_sec: float
        -lock: threading.Lock
        -last_call: float
        -remaining_requests: float | None
        +__enter__()
        +__exit__()
    }
    MbInterface --> RateLimiter
Loading

File-Level Changes

Change Details Files
Introduce a centralized mbzero-based MusicBrainz interface
  • Add MbInterface class with lookup, browse, search, send, JSON cleaning, and query building
  • Add SharedMbInterface singleton to manage shared config, auth, and rate limiter
  • Integrate mbzero request and error classes throughout
beetsplug/_mb_interface.py
Migrate plugins to use MbInterface instead of musicbrainzngs
  • Import and initialize SharedMbInterface in plugins
  • Replace direct musicbrainzngs.* calls with mb_interface.* methods
  • Update exception catching to use mbzerror classes
  • Remove old musicbrainzngs imports and related setup
beetsplug/musicbrainz.py
beetsplug/parentwork.py
beetsplug/mbcollection.py
beetsplug/listenbrainz.py
beetsplug/missing.py
Standardize JSON key conventions for mbzero output
  • Rename media and track list keys (e.g. medium-listmedia, track-listtracks, data-track-listdata-tracks)
  • Rename relation and alias keys (e.g. alias-listaliases, artist-relation-listartist-relations, release-relation-listrelease-relations, url-relation-listurl-relations)
  • Adjust parsing logic in plugins and update test fixtures accordingly
test/plugins/test_musicbrainz.py
test/plugins/test_parentwork.py
Replace musicbrainzngs dependency with mbzero and clean configs
  • Remove musicbrainzngs from pyproject.toml and docs, add mbzero entry
  • Remove set_useragent, hostname, and rate_limit calls from plugins
  • Simplify plugin config to rely on SharedMbInterface
pyproject.toml
docs/plugins/musicbrainz.rst
docs/plugins/mbcollection.rst
docs/plugins/missing.rst
docs/plugins/listenbrainz.rst
docs/plugins/parentwork.rst
Add a thread-safe RateLimiter utility with tests
  • Implement RateLimiter class controlling request start rate in util
  • Add multithreaded and single-thread tests to ensure correct pacing
beets/util/rate_limiter.py
test/util/test_rate_limiter.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Consider abstracting the repetitive JSON field renaming logic (e.g. mapping 'medium-list'→'media', 'track-list'→'tracks', etc.) into a shared transformation utility to reduce boilerplate and improve consistency.
  • Most of the new MbInterface public methods are missing explicit return type annotations; adding these will improve readability and help catch type errors earlier.
  • The custom RateLimiter uses sleep loops under a lock; consider switching to a condition-based wait or token-bucket approach to avoid potential thread contention and improve responsiveness under high concurrency.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider abstracting the repetitive JSON field renaming logic (e.g. mapping 'medium-list'→'media', 'track-list'→'tracks', etc.) into a shared transformation utility to reduce boilerplate and improve consistency.
- Most of the new MbInterface public methods are missing explicit return type annotations; adding these will improve readability and help catch type errors earlier.
- The custom RateLimiter uses sleep loops under a lock; consider switching to a condition-based wait or token-bucket approach to avoid potential thread contention and improve responsiveness under high concurrency.

## Individual Comments

### Comment 1
<location> `beetsplug/musicbrainz.py:862` </location>
<code_context>
             )

         # release is potentially a pseudo release
-        release = self.album_info(res["release"])
+        release = self.album_info(res)

         # should be None unless we're dealing with a pseudo release
</code_context>

<issue_to_address>
Passing the entire response instead of the 'release' key may affect downstream logic.

Verify that album_info is designed to handle the full response object; otherwise, this change may cause runtime errors or incomplete data extraction.
</issue_to_address>

### Comment 2
<location> `beetsplug/_mb_interface.py:138` </location>
<code_context>
+            scheme = "https" if self.https else "http"
+            mbr.set_url(f"{scheme}://{self.hostname}/ws/2")
+        opts = {}
+        if limit:
+            opts["limit"] = limit
+        if offset:
+            opts["offset"] = offset
+        return mbr.send(opts=opts, credentials=self.auth)
</code_context>

<issue_to_address>
Using 'if limit:' and 'if offset:' may skip zero values, which could be valid for paging.

Use 'if limit is not None' and 'if offset is not None' to ensure zero values are included in opts.
</issue_to_address>

### Comment 3
<location> `test/plugins/test_musicbrainz.py:1042` </location>
<code_context>

     def test_candidates(self, monkeypatch, mb):
         monkeypatch.setattr(
-            "musicbrainzngs.search_releases",
</code_context>

<issue_to_address>
Missing test for error handling in candidate search.

Add a test where MbInterface.search_releases raises an exception to confirm MusicBrainzAPIError is properly raised and handled.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

assert len(candidates) == 1
assert candidates[0].track_id == self.RECORDING["id"]

def test_candidates(self, monkeypatch, mb):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing test for error handling in candidate search.

Add a test where MbInterface.search_releases raises an exception to confirm MusicBrainzAPIError is properly raised and handled.

except mbzerror.MbzWebServiceError as exc:
raise MusicBrainzAPIError(
exc, f"{query_type} search", filters, traceback.format_exc()
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
)
) from exc

except mbzerror.MbzWebServiceError as exc:
raise MusicBrainzAPIError(
exc, "get release by ID", albumid, traceback.format_exc()
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
)
) from exc

except mbzerror.MbzWebServiceError as exc:
raise MusicBrainzAPIError(
exc, "get recording by ID", trackid, traceback.format_exc()
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
)
) from exc

@nicomem nicomem force-pushed the feature/mbzero-squashed branch from 793a36c to 32468f1 Compare August 30, 2025 09:10
@nicomem
Copy link
Contributor Author

nicomem commented Aug 30, 2025

Applied some of the suggestions and fixed the problems indentified in the CI jobs. For the ones I did not resolve, it was some existing code so I prefer leaving as-is to avoid any risk unless told otherwise

@nicomem nicomem force-pushed the feature/mbzero-squashed branch from 32468f1 to 624cddd Compare August 30, 2025 09:38
@nicomem
Copy link
Contributor Author

nicomem commented Aug 30, 2025

Fixed tests instabilities by removing the rate-limiter checks of requests not executed too late (because threads may sleep for more time than requested).

Failure on 3.9 due to mbzero using type syntax not present in that version -> to fix in mbzero and release a new version

@nicomem nicomem force-pushed the feature/mbzero-squashed branch from 624cddd to 5a75bab Compare September 1, 2025 20:28
Louis Rannou and others added 6 commits September 1, 2025 22:43
`mbzero` is a lighter interface to access the MusicBrainz API, so a
`MbInterface` has been created to re-add similar functions that
`musicbrainzngs` provided.

Create a shared `MbInterface` outside the plugin so that it can later be
reused by other plugins, using the same rate limiter.
- Instead of the `musicbrainz` plugin reading the shared config values,
  it is now `MbInterface` that does it. This makes sure these values are
  correctly handled in case the `musicbrainz` plugin is disabled, but
  not the others.

Rate limiting is not done by `mbzero`, so it has been implemented here.
The implementation is not specific to `MbInterface` and may be reused by
others if needed in the future.
- A rate-limiting bug has also been fixed compared to the
  `musicbrainzngs` implementation
  - See issue 294 on `python-musicbrainzngs`

Tests have been modified as the structure of data received is a bit
different than before.

Signed-off-by: Louis Rannou <[email protected]>
Signed-off-by: Nicolas Mémeint <[email protected]>
This migration required adding a new `browse_release_groups` to the
`MbInterface`, but this is trivial.
Also remove the `musicbrainzngs` dependency now that it is not used
anymore
@nicomem nicomem force-pushed the feature/mbzero-squashed branch from 5a75bab to d0b9f8f Compare September 1, 2025 20:43
@nicomem nicomem changed the title Replace usage of the musicbrainzbgs library with mbzero Replace usage of the musicbrainzngs library with mbzero Sep 6, 2025
@Louson Louson mentioned this pull request Sep 7, 2025
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant