Migrate parentwork to use MusicBrainzAPI

This commit is contained in:
Šarūnas Nejus 2025-12-22 16:45:15 +00:00
parent 741f5c4be1
commit a33371b6ef
No known key found for this signature in database
7 changed files with 106 additions and 122 deletions

View file

@ -66,7 +66,7 @@ jobs:
- if: ${{ env.IS_MAIN_PYTHON != 'true' }}
name: Test without coverage
run: |
poetry install --without=lint --extras=autobpm --extras=lyrics --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate --extras=parentwork
poetry install --without=lint --extras=autobpm --extras=lyrics --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate
poe test
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}
@ -74,7 +74,7 @@ jobs:
env:
LYRICS_UPDATED: ${{ steps.lyrics-update.outputs.any_changed }}
run: |
poetry install --extras=autobpm --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate --extras=parentwork
poetry install --extras=autobpm --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate
poe docs
poe test-with-coverage

View file

@ -91,6 +91,9 @@ class MusicBrainzAPI(RequestHandler):
def get_recording(self, id_: str, **kwargs) -> JSONDict:
return self.get_entity(f"recording/{id_}", **kwargs)
def get_work(self, id_: str, **kwargs) -> JSONDict:
return self.get_entity(f"work/{id_}", **kwargs)
def browse_recordings(self, **kwargs) -> list[JSONDict]:
return self.get_entity("recording", **kwargs)["recordings"]

View file

@ -20,50 +20,15 @@ from __future__ import annotations
from typing import Any
import musicbrainzngs
import requests
from beets import __version__, ui
from beets import ui
from beets.plugins import BeetsPlugin
musicbrainzngs.set_useragent("beets", __version__, "https://beets.io/")
from ._utils.musicbrainz import MusicBrainzAPIMixin
def find_parentwork_info(mb_workid: str) -> tuple[dict[str, Any], str | None]:
"""Get the MusicBrainz information dict about a parent work, including
the artist relations, and the composition date for a work's parent work.
"""
work_date = None
parent_id: str | None = mb_workid
while parent_id:
current_id = parent_id
work_info = musicbrainzngs.get_work_by_id(
current_id, includes=["work-rels", "artist-rels"]
)["work"]
work_date = work_date or next(
(
end
for a in work_info.get("artist-relation-list", [])
if a["type"] == "composer" and (end := a.get("end"))
),
None,
)
parent_id = next(
(
w["work"]["id"]
for w in work_info.get("work-relation-list", [])
if w["type"] == "parts" and w["direction"] == "backward"
),
None,
)
return musicbrainzngs.get_work_by_id(
current_id, includes=["artist-rels"]
), work_date
class ParentWorkPlugin(BeetsPlugin):
class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):
def __init__(self):
super().__init__()
@ -125,14 +90,13 @@ class ParentWorkPlugin(BeetsPlugin):
parentwork_info = {}
composer_exists = False
if "artist-relation-list" in work_info["work"]:
for artist in work_info["work"]["artist-relation-list"]:
if artist["type"] == "composer":
composer_exists = True
parent_composer.append(artist["artist"]["name"])
parent_composer_sort.append(artist["artist"]["sort-name"])
if "end" in artist.keys():
parentwork_info["parentwork_date"] = artist["end"]
for artist in work_info.get("artist-relations", []):
if artist["type"] == "composer":
composer_exists = True
parent_composer.append(artist["artist"]["name"])
parent_composer_sort.append(artist["artist"]["sort-name"])
if "end" in artist.keys():
parentwork_info["parentwork_date"] = artist["end"]
parentwork_info["parent_composer"] = ", ".join(parent_composer)
parentwork_info["parent_composer_sort"] = ", ".join(
@ -144,16 +108,14 @@ class ParentWorkPlugin(BeetsPlugin):
"no composer for {}; add one at "
"https://musicbrainz.org/work/{}",
item,
work_info["work"]["id"],
work_info["id"],
)
parentwork_info["parentwork"] = work_info["work"]["title"]
parentwork_info["mb_parentworkid"] = work_info["work"]["id"]
parentwork_info["parentwork"] = work_info["title"]
parentwork_info["mb_parentworkid"] = work_info["id"]
if "disambiguation" in work_info["work"]:
parentwork_info["parentwork_disambig"] = work_info["work"][
"disambiguation"
]
if "disambiguation" in work_info:
parentwork_info["parentwork_disambig"] = work_info["disambiguation"]
else:
parentwork_info["parentwork_disambig"] = None
@ -185,9 +147,9 @@ class ParentWorkPlugin(BeetsPlugin):
work_changed = item.parentwork_workid_current != item.mb_workid
if force or not hasparent or work_changed:
try:
work_info, work_date = find_parentwork_info(item.mb_workid)
except musicbrainzngs.musicbrainz.WebServiceError as e:
self._log.debug("error fetching work: {}", e)
work_info, work_date = self.find_parentwork_info(item.mb_workid)
except requests.exceptions.RequestException:
self._log.debug("error fetching work", item, exc_info=True)
return
parent_info = self.get_info(item, work_info)
parent_info["parentwork_workid_current"] = item.mb_workid
@ -228,3 +190,37 @@ class ParentWorkPlugin(BeetsPlugin):
"parentwork_date",
],
)
def find_parentwork_info(
self, mb_workid: str
) -> tuple[dict[str, Any], str | None]:
"""Get the MusicBrainz information dict about a parent work, including
the artist relations, and the composition date for a work's parent work.
"""
work_date = None
parent_id: str | None = mb_workid
while parent_id:
current_id = parent_id
work_info = self.mb_api.get_work(
current_id, includes=["work-rels", "artist-rels"]
)
work_date = work_date or next(
(
end
for a in work_info.get("artist-relations", [])
if a["type"] == "composer" and (end := a.get("end"))
),
None,
)
parent_id = next(
(
w["work"]["id"]
for w in work_info.get("work-relations", [])
if w["type"] == "parts" and w["direction"] == "backward"
),
None,
)
return work_info, work_date

View file

@ -38,16 +38,6 @@ This plugin adds seven tags:
to keep track of recordings whose works have changed.
- **parentwork_date**: The composition date of the parent work.
Installation
------------
To use the ``parentwork`` plugin, first enable it in your configuration (see
:ref:`using-plugins`). Then, install ``beets`` with ``parentwork`` extra
.. code-block:: bash
pip install "beets[parentwork]"
Configuration
-------------

3
poetry.lock generated
View file

@ -4185,7 +4185,6 @@ mbcollection = ["musicbrainzngs"]
metasync = ["dbus-python"]
missing = ["musicbrainzngs"]
mpdstats = ["python-mpd2"]
parentwork = ["musicbrainzngs"]
plexupdate = ["requests"]
reflink = ["reflink"]
replaygain = ["PyGObject"]
@ -4198,4 +4197,4 @@ web = ["flask", "flask-cors"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4"
content-hash = "d9141a482e4990a4466a121a59deaeaf46e5613ff0af315f277110935e391e63"
content-hash = "dbe3785cbffd71f2ca758872f7654522228d6155c76a8f003bec22f03c8eada3"

View file

@ -168,7 +168,6 @@ mbcollection = ["musicbrainzngs"]
metasync = ["dbus-python"]
missing = ["musicbrainzngs"]
mpdstats = ["python-mpd2"]
parentwork = ["musicbrainzngs"]
plexupdate = ["requests"]
reflink = ["reflink"]
replaygain = [

View file

@ -14,9 +14,6 @@
"""Tests for the 'parentwork' plugin."""
from typing import Any
from unittest.mock import Mock, patch
import pytest
from beets.library import Item
@ -74,56 +71,56 @@ class ParentWorkIntegrationTest(PluginTestCase):
assert item["mb_parentworkid"] == "XXX"
def mock_workid_response(mbid, includes):
works: list[dict[str, Any]] = [
{
"id": "1",
"title": "work",
"work-relation-list": [
{
"type": "parts",
"direction": "backward",
"work": {"id": "2"},
}
],
},
{
"id": "2",
"title": "directparentwork",
"work-relation-list": [
{
"type": "parts",
"direction": "backward",
"work": {"id": "3"},
}
],
},
{
"id": "3",
"title": "parentwork",
},
]
return {
"work": {
**next(w for w in works if mbid == w["id"]),
"artist-relation-list": [
{
"type": "composer",
"artist": {
"name": "random composer",
"sort-name": "composer, random",
},
}
],
}
}
@patch("musicbrainzngs.get_work_by_id", Mock(side_effect=mock_workid_response))
class ParentWorkTest(PluginTestCase):
plugin = "parentwork"
@pytest.fixture(autouse=True)
def patch_works(self, requests_mock):
requests_mock.get(
"/ws/2/work/1?inc=work-rels%2Bartist-rels",
json={
"id": "1",
"title": "work",
"work-relations": [
{
"type": "parts",
"direction": "backward",
"work": {"id": "2"},
}
],
},
)
requests_mock.get(
"/ws/2/work/2?inc=work-rels%2Bartist-rels",
json={
"id": "2",
"title": "directparentwork",
"work-relations": [
{
"type": "parts",
"direction": "backward",
"work": {"id": "3"},
}
],
},
)
requests_mock.get(
"/ws/2/work/3?inc=work-rels%2Bartist-rels",
json={
"id": "3",
"title": "parentwork",
"artist-relations": [
{
"type": "composer",
"artist": {
"name": "random composer",
"sort-name": "composer, random",
},
}
],
},
)
def test_normal_case(self):
item = Item(path="/file", mb_workid="1", parentwork_workid_current="1")
item.add(self.lib)