beets icon indicating copy to clipboard operation
beets copied to clipboard

Make musicbrainz plugin talk to musicbrainz directly

Open snejus opened this issue 3 months ago • 6 comments

This PR refactors the MusicBrainz plugin implementation by replacing the musicbrainzngs library with direct HTTP API calls using requests and requests-ratelimiter.

Key Changes:

  • New utilities module: Added beetsplug/_utils/requests.py with TimeoutSession class and HTTP error handling (HTTPNotFoundError, CaptchaError)
  • MusicBrainz API rewrite: Replaced musicbrainzngs dependency with custom MusicBrainzAPI class using direct HTTP requests
  • Rate limiting: Integrated requests-ratelimiter for API rate limiting instead of musicbrainzngs.set_rate_limit()
  • Data structure updates: Updated field names to match MusicBrainz JSON API v2 format (e.g., medium-listmedia, track-listtracks)
  • Dependency management:
    • Made musicbrainzngs optional and added it to plugin-specific extras (listenbrainz, mbcollection, missing, parentwork). Updated plugin docs accordingly.
    • Made requests a required dependency to ensure backwards compatibility (ideally, we would make it an optional dependency under musicbrainz extra).
  • Error handling: Simplified error handling by removing MusicBrainzAPIError wrapper class

Benefits:

  • Direct control over HTTP requests
  • Consistent rate limiting across all network requests
  • Better alignment with modern MusicBrainz API responses

The changes maintain backward compatibility while modernizing the underlying implementation.

snejus avatar Sep 29 '25 00:09 snejus

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.

github-actions[bot] avatar Sep 29 '25 00:09 github-actions[bot]

Codecov Report

:x: Patch coverage is 84.34783% with 36 lines in your changes missing coverage. Please review. :white_check_mark: Project coverage is 68.36%. Comparing base (ac0b6ec) to head (5785ce3). :warning: Report is 16 commits behind head on master. :white_check_mark: All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/musicbrainz.py 76.61% 21 Missing and 8 partials :warning:
beetsplug/lyrics.py 83.33% 4 Missing and 1 partial :warning:
beetsplug/_utils/requests.py 98.50% 0 Missing and 1 partial :warning:
beetsplug/mbpseudo.py 88.88% 0 Missing and 1 partial :warning:
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6052      +/-   ##
==========================================
+ Coverage   68.03%   68.36%   +0.32%     
==========================================
  Files         137      138       +1     
  Lines       18708    18773      +65     
  Branches     3163     3172       +9     
==========================================
+ Hits        12728    12834     +106     
+ Misses       5309     5263      -46     
- Partials      671      676       +5     
Files with missing lines Coverage Δ
beetsplug/_utils/requests.py 98.50% <98.50%> (ø)
beetsplug/mbpseudo.py 80.00% <88.88%> (+0.37%) :arrow_up:
beetsplug/lyrics.py 85.12% <83.33%> (+6.82%) :arrow_up:
beetsplug/musicbrainz.py 81.15% <76.61%> (+1.98%) :arrow_up:
:rocket: New features to boost your workflow:
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar Sep 29 '25 00:09 codecov[bot]

I think there's also potential to be able to use the new request code here and refactor the Discogs plugin to make direct API calls as well, since we end up fetching the whole release object and parsing it like a dictionary anyway.

henry-oberholtzer avatar Sep 29 '25 15:09 henry-oberholtzer

It may replace https://github.com/beetbox/beets/pull/5976 implementation over mbzero.

Morikko avatar Sep 29 '25 16:09 Morikko

It may replace #5976 implementation over mbzero.

Peharps this can be discussed in the original thread #4660

EDIT: I have updated the thread to resume the situation

Louson avatar Oct 08 '25 16:10 Louson

It may replace #5976 implementation over mbzero.

I would think so, given that this requires no external dependencies and is simpler.

snejus avatar Oct 20 '25 10:10 snejus

I tested these adjustments by reimporting all musicbrainz tracks/albums in my library

$ beet -vv import -LI data_source:musicbrainz --plugins=musicbrainz,importreplace (and -s)

I found a couple of issues and fixed them, see the diff:

  • Added retry on connection errors (3 attempts, 1s, 2s and 4s later)
  • Fixed inc parameter format
  • Fixed URLs parsing

snejus avatar Dec 20 '25 02:12 snejus

That retry on connection might really help cut down on currently the persistent Musicbrainz unreachable error! Nice!

henry-oberholtzer avatar Dec 20 '25 02:12 henry-oberholtzer

Have also added a test for the retry behaviour. Pretty sure it's ready now, merging!

snejus avatar Dec 21 '25 00:12 snejus

Hi! I'm getting this error with the parentwork plugin:

$beet parentwork -f
parentwork: No work for 10 String Symphony - 10 String Symphony - Prettiest Girl, add one at https://musicbrainz.org/recording/9cebf2bd-8fef-432a-b360-23d70ab03008
Traceback (most recent call last):
  File "/home/user/.local/bin/beet", line 10, in <module>
    sys.exit(main())
             ~~~~^^
  File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 1631, in main
    _raw_main(args)
    ~~~~~~~~~^^^^^^
  File "/home/user/.local/lib/beets/beets/ui/__init__.py", line 1610, in _raw_main
    subcommand.func(lib, suboptions, subargs)
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/.local/lib/beets/beetsplug/parentwork.py", line 92, in func
    changed = self.find_work(item, force_parent, verbose=True)
  File "/home/user/.local/lib/beets/beetsplug/parentwork.py", line 193, in find_work
    work_info, work_date = find_parentwork_info(item.mb_workid)
                           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/home/user/.local/lib/beets/beetsplug/parentwork.py", line 64, in find_parentwork_info
    parent_id, work_date = work_parent_id(mb_workid)
                           ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/home/user/.local/lib/beets/beetsplug/parentwork.py", line 53, in work_parent_id
    new_mb_workid, work_date = direct_parent_id(mb_workid, work_date)
                               ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/.local/lib/beets/beetsplug/parentwork.py", line 29, in direct_parent_id
    work_info = musicbrainzngs.get_work_by_id(
        mb_workid, includes=["work-rels", "artist-rels"]
    )
  File "/usr/lib/python3.13/site-packages/musicbrainzngs/musicbrainz.py", line 904, in get_work_by_id
    return _do_mb_query("work", id, includes)
  File "/usr/lib/python3.13/site-packages/musicbrainzngs/musicbrainz.py", line 728, in _do_mb_query
    return _mb_request(path, 'GET', auth_required, args=args)
  File "/usr/lib/python3.13/site-packages/musicbrainzngs/musicbrainz.py", line 417, in __call__
    return self.fun(*args, **kwargs)
           ~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/musicbrainzngs/musicbrainz.py", line 624, in _mb_request
    raise UsageError("set a proper user-agent with "
                     "set_useragent(\"application name\", \"application version\", \"contact info (preferably URL or email for your application)\")")
musicbrainzngs.musicbrainz.UsageError: set a proper user-agent with set_useragent("application name", "application version", "contact info (preferably URL or email for your application)")

It looks like user-agent information was removed here: https://github.com/beetbox/beets/pull/6052/files#diff-3420b9b69bb3555786b2100c434079c83f948e7f1dc796869f4945c33577d3baL87 . Should this now be set elsewhere, or should the parentwork (and presumably other) plugins be refactored to use a similar approach as to here?

aereaux avatar Dec 21 '25 19:12 aereaux

Looks like we didn't catch that side effect!

I think it'd be nice to get that plugin & others either using our new request system or fold it into the Musicbrainz plugin. In the interim we can probably patch in a user agent?

henry-oberholtzer avatar Dec 21 '25 19:12 henry-oberholtzer

I might be able to do some work on parentwork in the next couple weeks. Or would the better plan be to include it directly in the musicbrainz plugin?

aereaux avatar Dec 21 '25 21:12 aereaux

Oops, apologies!

I'm currently working on migrating the rest of plugins away from musicbrainzngs so I will take care of this :)

snejus avatar Dec 21 '25 21:12 snejus

Great! I can test anything out when you have it!

aereaux avatar Dec 21 '25 22:12 aereaux