-
Notifications
You must be signed in to change notification settings - Fork 2k
Return candidates from all data sources on id search #6184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e14047a
34ea935
e1c05ee
ab4fad9
016661d
2da99e8
497e97f
b73d48a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
|
|
||
| from typing_extensions import Self | ||
|
|
||
| from beets import plugins | ||
| from beets.util import cached_classproperty | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -58,6 +59,16 @@ def __hash__(self) -> int: # type: ignore[override] | |
| class Info(AttrDict[Any]): | ||
| """Container for metadata about a musical entity.""" | ||
|
|
||
| Identifier = tuple[str | None, str | None] | ||
|
|
||
| @property | ||
| def id(self) -> str | None: | ||
| raise NotImplementedError | ||
|
|
||
| @property | ||
| def identifier(self) -> Identifier: | ||
snejus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return (self.data_source, self.id) | ||
|
|
||
| @cached_property | ||
| def name(self) -> str: | ||
| raise NotImplementedError | ||
|
|
@@ -103,6 +114,10 @@ class AlbumInfo(Info): | |
| user items, and later to drive tagging decisions once selected. | ||
| """ | ||
|
|
||
| @property | ||
| def id(self) -> str | None: | ||
| return self.album_id | ||
|
|
||
| @cached_property | ||
| def name(self) -> str: | ||
| return self.album or "" | ||
|
|
@@ -179,6 +194,10 @@ class TrackInfo(Info): | |
| stand alone for singleton matching. | ||
| """ | ||
|
|
||
| @property | ||
| def id(self) -> str | None: | ||
| return self.track_id | ||
|
|
||
| @cached_property | ||
| def name(self) -> str: | ||
| return self.title or "" | ||
|
|
@@ -247,6 +266,9 @@ class AlbumMatch(Match): | |
| extra_items: list[Item] | ||
| extra_tracks: list[TrackInfo] | ||
|
|
||
| def __post_init__(self) -> None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would move the event trigger out of the class constructor. It is possible that an Match object is constructed independent of a beets pipeline. I do not want to couple this that strongly if not necessary. E.g. we have some serialization logic in beets-flask and I don't want to trigger this whenever I load an Match entry for cold storage.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get the concern about this side effect but moving this to
This is actually a textbook use of Re: beets-flask serialization: match = object.__new__(AlbumMatch)
match.__dict__.update(serialized_data)Or even better, serialize just the match data rather than the objects themselves. If there's broader need for match serialization, we could discuss adding a proper public API for it. I want to avoid blocking necessary refactoring of beets' internals based on downstream usage of internal classes. Does the deserialization workaround work for your use case?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think the event should not be sent on every initialization. To me, the event is more closely coupled with the tagging logic than with the match object itself, although I do agree that the current approach makes the code a bit cleaner.
How does one identify public api in this case? We do not use Historically beets has treated only the plugin API as public, yes, but the project is also used as a library, and I think that use case deserves consideration as well. Without explicit boundaries, users reasonably assume that importable classes are fair game.
Just to clarify: I meant only deserialization. This was what the "load from cold storage" comment was referencing.
We routinely do block changes, or at least adjust them, because of potential downstream usage. Wanting to avoid that concern here feels a bit inconsistent. The workaround would work, but it shifts the burden entirely onto downstream users and breaks existing programs. I’m not opposed to the change in principle, and I don’t think it needs to block progress, but by any reasonable definition, this is a breaking change. In my view that implies either a major-version bump or, alternatively, introducing a minimal public deserialization format so maintainers can refactor freely without silently breaking consumers.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair points - you're right about the API ambiguity and that beets is used as a library. How about adding a @classmethod
def from_dict(cls, data: dict) -> AlbumMatch:
"""Reconstruct from serialized data without emitting events."""
obj = object.__new__(cls)
obj.__dict__.update(data)
return objThis gives library users a stable deserialization path, keeps the |
||
| plugins.send("album_matched", match=self) | ||
|
|
||
| @property | ||
| def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]: | ||
| return list(self.mapping.items()) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not like that we raise an NotImplementedError here. We should make the
Infoclass abstract or a protocol if want to define a contract for the inheritance.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered using
ABC+@abstractmethodhere, but opted against it for these reasons:Limited scope:
Infoonly has 2 concrete subclasses - it's not a public plugin interface where we need strict enforcement at instantiation time.Template pattern, not an interface: The base class provides real shared functionality (
identifierproperty,__repr__, common__init__parameters). Theidandnameproperties are just internal adapters mapping to different field names in subclasses (e.g.,album_idvstrack_id).Testing overhead: Making it an ABC would require either creating stub implementations or monkeypatching
__abstractmethods__in tests. Since we're not exposingInfofor external extension, the ceremony doesn't add value.The
NotImplementedErrorapproach clearly documents "subclasses must override this" without the ABC machinery. If we later exposeInfofor some plugin extendability, I'd absolutely convert it to ABC at that point.Happy to reconsider if you feel strongly about it though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Abstract classes or protocols can also provide real shared functionality.
We actually don’t construct raw Info instances in tests. Tests only ever instantiate AlbumInfo or TrackInfo directly. So converting Info to an ABC wouldn’t add test-work in practice.
It looks like this usage has already begun showing up in plugins (mbpseudo.py is one example). Given that the system is already being extended, it might be safer to formalize the interface now rather than later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's some confusion here -
mbpseudo.pydoesn't subclassInfo. It's a metadata source plugin that returnsAlbumInfo/TrackInfoinstances, but it doesn't create newInfosubclasses.On the other hand,
discogsdoes - however, that approach is being reverted in #6179 since this subclass unintentionally introduced flexible attributes that ended up written into the database.Given the above (that they actually should not be subclassed), I'd prefer to keep it simple with
NotImplementedErrorfor now. We can formalize it as an ABC if/when there's an actual need for plugin extensibility.