from __future__ import annotations
import copy
from typing import TYPE_CHECKING
from ..._shared import TTLCache, _copy_docstring
from ._shared import MusixmatchResourceAPI
from .tracks import TracksAPI
if TYPE_CHECKING:
from datetime import datetime
from typing import Any
[docs]
class EnterpriseAPI(MusixmatchResourceAPI):
"""
Enterprise API endpoints for the Musixmatch Lyrics API.
.. important::
This class is managed by
:class:`~minim.api.musixmatch.MusixmatchLyricsAPIClient` and
should not be instantiated directly.
"""
_RIGHTSHOLDER_ROLES = {
"A",
"AD",
"AM",
"AR",
"AQ",
"C",
"CA",
"E",
"ES",
"PA",
"PR",
"SA",
"SE",
"SR",
"TR",
}
__slots__ = ()
@staticmethod
def _process_work_rightsholders(
rightsholders: list[dict[str, Any]], rightsholder_type: str, /
) -> None:
"""
Process musical work rightsholders.
Parameters
----------
rightsholders : list[dict[str, Any]]; positional-only
Musical work publishers or writers.
rightsholder_type : str; positional-only
Type of musical work rightsholder.
**Valid values**: :code:`"publisher"`, :code:`"writer"`.
"""
attr_prefix = f"work_data['owners'][{rightsholder_type!r}]"
MusixmatchResourceAPI._validate_type(attr_prefix, rightsholders, list)
for idx, rightsholder in enumerate(rightsholders):
item_name = f"{attr_prefix}[{idx}]"
MusixmatchResourceAPI._validate_type(item_name, rightsholder, dict)
required_keys = {"name", "controlled"}
for attr_key, attr_val in rightsholder.items():
attr_name = f"{item_name}[{attr_key!r}]"
match attr_key:
# data.owners.(publisher|writer).name
case "name":
rightsholder[attr_key] = (
MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
)
)
required_keys.remove(attr_key)
# data.owners.(publisher|writer).controlled
case "controlled":
attr_val = MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
).upper()
if attr_val not in "NY":
raise ValueError(
f"`{attr_name}` must be either 'Y' or 'N'."
)
rightsholder[attr_key] = attr_val
required_keys.remove(attr_key)
# data.owners.(publisher|writer).identifier
case "identifier":
rightsholder[attr_key] = (
MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
)
)
# data.owners.(publisher|writer).ipi
case "ipi":
attr_val = rightsholder[attr_key] = (
MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
)
if isinstance(attr_val, str)
else str(attr_val)
)
MusixmatchResourceAPI._validate_numeric(
attr_name, attr_val, int
)
# data.owners.(publisher|writer).role
case "role":
attr_val = rightsholder[attr_key] = (
MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
).upper()
)
if attr_val not in EnterpriseAPI._RIGHTSHOLDER_ROLES:
raise ValueError(
f"Invalid role {attr_val!r} for "
f"`{item_name}`. Valid values: "
f"{MusixmatchResourceAPI._join_values(EnterpriseAPI._RIGHTSHOLDER_ROLES)}."
)
# data.owners.(publisher|writer).(mech|perf|sync)_ownership_share
case (
"mech_ownership_share"
| "perf_ownership_share"
| "sync_ownership_share"
):
MusixmatchResourceAPI._validate_number(
attr_key, attr_val, int, 0, 10_000
)
case _:
raise ValueError(
f"Invalid key {attr_key!r} in the "
f"`{attr_name}` dictionary."
)
if required_keys:
raise ValueError(
f"`{attr_name}` is missing the following required key(s): "
f"{MusixmatchResourceAPI._join_values(required_keys)}."
)
@staticmethod
def _process_work_owners(owners: dict[str, Any] | None, /) -> None:
"""
Process musical work owners.
Parameters
----------
owners : dict[str, Any] or None; positional-only
Musical work owners.
"""
if owners is None:
return
MusixmatchResourceAPI._validate_type(
"work_data['owners']", owners, dict
)
required_keys = {"publisher", "writer"}
for attr_key, attr_val in owners.items():
match attr_key:
# data.owners.(publisher|writer)
case "publisher" | "writer":
EnterpriseAPI._process_work_rightsholders(
attr_val, attr_key
)
required_keys.remove(attr_key)
case _:
raise ValueError(
f"Invalid key {attr_key!r} in the "
"`work_data['owners']` dictionary."
)
if required_keys:
raise ValueError(
"`work_data['owners']` is missing the following "
"required key(s): "
f"{MusixmatchResourceAPI._join_values(required_keys)}."
)
@staticmethod
def _process_work_territories(
territories: list[dict[str, Any]], /
) -> None:
"""
Process musical work royalty collection territories.
Parameters
----------
territories : list[dict[str, Any]]; positional-only
Musical work royalty collection territories.
"""
attr_prefix = "work_data['owners']['territories']"
MusixmatchResourceAPI._validate_type(attr_prefix, territories, list)
for idx, territory in enumerate(territories):
item_name = f"{attr_prefix}[{idx}]"
MusixmatchResourceAPI._validate_type(item_name, territory, dict)
required_keys = {"code"}
optional_keys = {"mech_share", "perf_share", "sync_share"}
for attr_key, attr_val in territory.items():
attr_name = f"{item_name}[{attr_key!r}]"
match attr_key:
case "code":
MusixmatchResourceAPI._validate_country_code(attr_val)
required_keys.remove(attr_key)
case "mech_share" | "perf_share" | "sync_share":
MusixmatchResourceAPI._validate_number(
attr_name, attr_val, int, 0, 10_000
)
optional_keys.discard(attr_key)
case _:
raise ValueError(
f"Invalid key {attr_key!r} in the "
f"`{attr_name}` dictionary."
)
if required_keys:
raise ValueError(
f"`{attr_name}` is missing the following required key(s): "
f"{MusixmatchResourceAPI._join_values(required_keys)}."
)
if len(optional_keys) == 3:
raise ValueError(
f"`{attr_name}` requires at least one of the "
"following key(s): "
f"{MusixmatchResourceAPI._join_values(optional_keys)}."
)
@staticmethod
def _process_work_collection(collection: dict[str, Any] | None, /) -> None:
"""
Process musical work royalty collection.
Parameters
----------
collection : dict[str, Any] or None; positional-only
Musical work royalty collection.
"""
if collection is None:
return
MusixmatchResourceAPI._validate_type(
"work_data['collection']", collection, dict
)
required_keys = {"territories"}
for attr_key, attr_val in collection.items():
match attr_key:
# data.collection.territories
case "territories":
EnterpriseAPI._process_work_territories(attr_val)
required_keys.remove(attr_key)
# data.collection.validity_(begin|end)
case "validity_begin" | "validity_end":
collection[attr_key] = (
MusixmatchResourceAPI._prepare_datetime(
attr_val, "%Y-%m-%d"
)
)
case _:
raise ValueError(
f"Invalid key {attr_key!r} in the "
"`work_data['collection']` dictionary."
)
if required_keys:
raise ValueError(
"`work_data['collection']` is missing the following "
"required key(s): "
f"{MusixmatchResourceAPI._join_values(required_keys)}."
)
@staticmethod
def _prepare_work_data(work_data: dict[str, Any]) -> dict[str, Any]:
"""
Validate and normalize musical work data.
Parameters
----------
work_data : dict[str, Any]
Musical work data.
Returns
-------
work_data : dict[str, Any]
Normalized musical work data.
"""
MusixmatchResourceAPI._validate_type("work_data", work_data, dict)
work_data = copy.deepcopy(work_data)
required_keys = {"identifier", "title"}
for attr_key, attr_val in work_data.items():
attr_name = f"work_data[{attr_key!r}]"
match attr_key:
# data.identifier
case "identifier":
attr_val = work_data[attr_key] = (
MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
)
)
if len(attr_val) > 40:
raise ValueError(
f"`{attr_name}` must be between 1 and 40 "
"characters long."
)
required_keys.remove(attr_key)
# data.title
case "title":
work_data[attr_key] = (
MusixmatchResourceAPI._prepare_string(
attr_name, attr_val
)
)
required_keys.remove(attr_key)
# data.alternate_titles
case "alternative_titles":
MusixmatchResourceAPI._validate_type(
attr_name, attr_val, list
)
work_data[attr_key] = [
MusixmatchResourceAPI._prepare_string(
f"{attr_name}[{idx}]", title
)
for idx, title in enumerate(attr_val)
]
# data.iswc
case "iswc":
work_data[attr_key] = MusixmatchResourceAPI._prepare_iswc(
attr_val
)
# data.isrc
case "isrc":
work_data[attr_key] = MusixmatchResourceAPI._prepare_isrc(
attr_val
)
# data.performers
case "performers":
MusixmatchResourceAPI._validate_type(
attr_name, attr_val, dict
)
required_subkeys = {"name"}
for subattr_key, subattr_val in attr_val.items():
subattr_name = f"{attr_name}[{subattr_key!r}]"
match subattr_key:
case "name":
attr_val[subattr_key] = (
MusixmatchResourceAPI._prepare_string(
subattr_name, subattr_val
)
)
required_subkeys.remove(subattr_key)
case "identifier":
attr_val[subattr_key] = (
MusixmatchResourceAPI._prepare_string(
subattr_name, subattr_val
)
)
case _:
raise ValueError(
f"Invalid key {subattr_key!r} in the "
f"`{attr_name}` dictionary."
)
if required_subkeys:
raise ValueError(
f"`{attr_name}` is missing the following "
"required key(s): "
f"{MusixmatchResourceAPI._join_values(required_subkeys)}."
)
# data.owners
case "owners":
EnterpriseAPI._process_work_owners(attr_val)
# data.collection
case "collection":
EnterpriseAPI._process_work_collection(attr_val)
# data.lyrics
case "lyrics":
MusixmatchResourceAPI._validate_type(
attr_name, attr_val, dict
)
required_subkeys = {"lyrics"}
for subattr_key, subattr_val in attr_val.items():
subattr_name = f"{attr_name}[{subattr_key!r}]"
match subattr_key:
# data.lyrics.(lyrics|lrc)
case "lyrics" | "lrc":
MusixmatchResourceAPI._validate_type(
subattr_name, subattr_val, str
)
# data.lyrics.duration
case "duration":
MusixmatchResourceAPI._validate_number(
subattr_name, subattr_val, int, 0
)
case _:
raise ValueError(
f"Invalid key {subattr_key!r} in the "
f"`{attr_name}` dictionary."
)
if required_subkeys:
raise ValueError(
f"`{attr_name}` is missing the following "
"required key(s): "
f"{MusixmatchResourceAPI._join_values(required_subkeys)}."
)
case _:
raise ValueError(
f"Invalid key {attr_key!r} in the `work_data` "
"dictionary."
)
if required_keys:
raise ValueError(
"`work_data` is missing the following required key(s): "
f"{MusixmatchResourceAPI._join_values(required_keys)}."
)
return work_data
[docs]
def submit_work(self, work_data: dict[str, Any], /) -> dict[str, Any]:
"""
`Enterprise > work.post <https://docs.musixmatch.com
/enterprise-integration/api-reference/work-post>`_: Submit
details for a musical work to Musixmatch.
.. admonition:: Subscription
:class: entitlement
.. tab-set::
.. tab-item:: Required
Musixmatch Enterprise plan
Access extended music metadata, advanced search,
translations, song structure, and lyric analysis.
`Learn more. <https://about.musixmatch.com
/api-pricing>`__
Parameters
----------
work_data : dict[str, Any]; positional-only
Details for a musical work.
.. seealso::
`Musixmatch Lyrics API documentation
<https://docs.musixmatch.com/enterprise-integration
/api-reference/work-post#body-data>`_ – Data schema.
Returns
-------
work : dict[str, Any]
Musixmatch catalog record for the newly submitted musical
work.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"message": {
"body": {
"alternate_titles": [
{
"title": <str>
}
],
"collections": {
<str>: {
"territories": [
{
"code": <str>,
"countries": <list[str]>,
"mech_share": <int>,
"perf_share": <int>,
"publisher": None,
"sync_share": <int>
}
],
"validity_begin": <str>,
"validity_end": None
}
},
"identifier": <str>,
"is_disabled": <int>,
"isrc": <list[str]>,
"iswc": <str>,
"last_trasmission": <str>,
"owners": {
"publisher": [
{
"controlled": <str>,
"id": <int>,
"identifier": <str>,
"ipi": <str>,
"mech_ownership_share": <int>,
"mech_society": None,
"name": <str>,
"perf_ownership_share": <int>,
"perf_society": None,
"role": None,
"sync_ownership_share": <int>,
"sync_society": None,
"type": "publisher",
"validity_begin": <str>
}
],
"writer": [
{
"controlled": <str>,
"id": <int>,
"identifier": <str>,
"ipi": <str>,
"mech_ownership_share": <int>,
"mech_society": None,
"name": <str>,
"perf_ownership_share": <int>,
"perf_society": None,
"role": None,
"sync_ownership_share": <int>,
"sync_society": None,
"type": "writer",
"validity_begin": <str>
}
]
},
"ownership": [],
"performers": [],
"publisher_short_name": <str>,
"source": {
"affiliation": None,
"control": <bool>,
"created": <str>,
"credits_priority": <str>,
"description": <str>,
"id": <int>,
"last_updated": <str>,
"report": <str>,
"sender_id": <str>,
"sender_name": <str>,
"short_name": <str>,
"type_of_right": <str>,
"validity_begin": <str>,
"validity_end": None
},
"submissions": [
{
"creation_date": <str>,
"disabled": <str>,
"filename": <str>,
"id": <int>,
"source": <int>,
"transmission_date": <str>
}
],
"tablespace": <str>,
"title": <str>,
"type_of_right": <str>,
"validity_begin": None,
"validity_end": <str>,
"wgid": <str>,
"work_id": <int>
},
"header": {
"execute_time": <float>,
"status_code": <int>
}
}
}
"""
self._client._require_api_key("enterprise.submit_work")
return self._client._request(
"POST",
"work.post",
headers={"content-type": "application/json"},
json=self._prepare_work_data(work_data),
).json()
[docs]
def set_work_validity(
self, work_identifier: str, /, valid_until: str | datetime
) -> None:
"""
`Enterprise > work.validity.post <https://docs.musixmatch.com
/enterprise-integration/api-reference/work-validity-post>`_:
Set the validity end date for a musical work on Musixmatch.
.. admonition:: Subscription
:class: entitlement
.. tab-set::
.. tab-item:: Required
Musixmatch Enterprise plan
Access extended music metadata, advanced search,
translations, song structure, and lyric analysis.
`Learn more. <https://about.musixmatch.com
/api-pricing>`__
Parameters
----------
work_identifier : str; positional-only
Unique identifier of the musical work.
**Valid length**: :code:`1` to :code:`40`.
**Example**: "00001100196005".
valid_until : str or datetime.datetime
Validity end date, in :code:`YYYY-MM-DD` format.
Returns
-------
status : dict[str, Any]
Whether the work's validity was updated successfully.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"message": {
"body": <str>,
"header": {
"execute_time": <float>,
"status_code": <int>
}
}
}
"""
self._client._require_api_key("enterprise.set_work_validity")
self._validate_type("work_identifier", work_identifier, str)
work_identifier = self._prepare_string(
"work_identifier", work_identifier
)
if not len(work_identifier) <= 40:
raise ValueError(
"`work_identifier` must be between 1 and 40 characters long."
)
return self._client._request(
"POST",
"work.validity.post",
headers={"content-type": "application/json"},
json={
"data": {
"identifier": work_identifier,
"validity_end": self._prepare_datetime(
valid_until, "%Y-%m-%d"
),
}
},
).json()
[docs]
@TTLCache.cached_method(ttl="daily")
def screen_track_lyrics(
self,
text: str,
/,
*,
max_candidates: int | None = None,
limit: int | None = None,
) -> dict[str, Any]:
"""
`Enterprise > track.lyrics.fingerprint.post
<https://docs.musixmatch.com/enterprise-integration
/api-reference/track-lyrics-fingerprint-post>`_: Screen text
against the Musixmatch catalog to identify tracks with matching
lyrics.
.. admonition:: Subscription
:class: entitlement
.. tab-set::
.. tab-item:: Required
Musixmatch Enterprise plan
Access extended music metadata, advanced search,
translations, song structure, and lyric analysis.
`Learn more. <https://about.musixmatch.com
/api-pricing>`__
Parameters
----------
text : str; positional-only
Text to screen for potential lyrical content.
max_candidates : int; keyword-only; optional
Maximum number of track candidates.
**Valid range**: :code:`1` to :code:`20`.
limit : int; keyword-only; optional
Maximum number of tracks to return.
**Valid range**: :code:`1` to `max_candidates`.
Returns
-------
tracks : dict[str, Any]
Musixmatch metadata for the identified tracks.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"message": {
"body": {
"track_list": [
{
"similarity": <float>,
"track": {
"album_coverart_100x100": <str>,
"album_coverart_350x350": <str>,
"album_coverart_500x500": <str>,
"album_coverart_800x800": <str>,
"album_id": <int>,
"album_name": <str>,
"artist_id": <int>,
"artist_name": <str>,
"commontrack_id": <int>,
"commontrack_isrcs": <list[list[str]]>,
"explicit": <int>,
"has_lyrics": <int>,
"has_richsync": <int>,
"has_subtitles": <int>,
"instrumental": <int>,
"num_favourite": <int>,
"primary_genres": {
"music_genre_list": [
{
"music_genre": {
"music_genre_id": <int>,
"music_genre_name": <str>,
"music_genre_name_extended": <str>,
"music_genre_parent_id": <int>,
"music_genre_vanity": <str>
}
}
]
},
"restricted": <int>,
"track_edit_url": <str>,
"track_id": <int>,
"track_isrc": <str>,
"track_length": <int>,
"track_name": <str>,
"track_rating": <int>,
"track_share_url": <str>,
"track_spotify_id": <str>,
"updated_time": <str>
}
}
]
},
"header": {
"execute_time": <float>,
"status_code": <int>
}
}
}
"""
self._client._require_api_key("enterprise.screen_track_lyrics")
params = {}
if max_candidates is not None:
self._validate_number("max_candidates", max_candidates, int, 1, 20)
params["size"] = max_candidates
if limit is not None:
self._validate_number(
"limit", limit, int, 1, params.get("size", 20)
)
params["limit"] = limit
return self._client._request(
"POST",
"track.lyrics.fingerprint.post",
headers={"content-type": "application/json"},
json={"data": {"text": text}},
params=params,
)
[docs]
@TTLCache.cached_method(ttl="static")
def get_track_catalog_record(self, isrc: str) -> dict[str, Any]:
"""
`Enterprise > track.dump.get <https://docs.musixmatch.com
/enterprise-integration/api-reference/track-dump-get>`_: Get
the Musixmatch catalog record for a track.
.. admonition:: Subscription
:class: entitlement
.. tab-set::
.. tab-item:: Required
Musixmatch Enterprise plan
Access extended music metadata, advanced search,
translations, song structure, and lyric analysis.
`Learn more. <https://about.musixmatch.com
/api-pricing>`__
Parameters
----------
isrc : str
ISRC of the track.
**Example**: :code:`"USUM70905526"`.
Returns
-------
track : dict[str, Any]
Musixmatch catalog record for the track.
.. admonition:: Sample reponse
:class: response dropdown
.. code-block::
{
"message": {
"body": [
{
"artist": <str>,
"commontrack_id": <int>,
"instrumental": <bool>,
"isrcs": <list[str]>,
"language_iso_code_1": <str>,
"last_updated": <str>,
"lyrics": <str>,
"lyrics_id": <int>,
"lyrics_tracking_url": <str>,
"restrictions": {
"allow": <list[str]>,
"blocked": <list[str]>
},
"snippet": <str>,
"subtitles": [
{
"body": <str>,
"id": <int>,
"length": <int>,
"tracking_url": <str>,
}
],
"title": <str>,
"writers": [
{
"id": <int>,
"name": <str>
}
]
}
],
"header": {
"execute_time": <float>,
"status_code": <int>
}
}
}
"""
self._client._require_api_key("enterprise.get_track_catalog_record")
return self._client._request(
"GET",
"track.dump.get",
params={"track_isrc": self._prepare_isrc(isrc)},
).json()
[docs]
@TTLCache.cached_method(ttl="daily")
def get_catalog_feeds(self) -> dict[str, Any]:
"""
`Enterprise > tracks.dump.get <https://docs.musixmatch.com
/enterprise-integration/api-reference/tracks-dump-get>`_: Get
Musixmatch resource information for the latest catalog feeds.
.. admonition:: Subscription
:class: entitlement
.. tab-set::
.. tab-item:: Required
Musixmatch Enterprise plan
Access extended music metadata, advanced search,
translations, song structure, and lyric analysis.
`Learn more. <https://about.musixmatch.com
/api-pricing>`__
Returns
-------
catalog_feeds : dict[str, Any]
Musixmatch metadata for the catalog feeds.
.. admonition:: Sample reponse
:class: response dropdown
.. code-block::
{
"message": {
"body": [
{
"created": <str>,
"download_url": <str>,
"full": <bool>,
"id": <int>
}
],
"header": {
"execute_time": <float>,
"status_code": <int>
}
}
}
"""
self._client._require_api_key("enterprise.get_catalog_feeds")
return self._client._request("GET", "tracks.dump.get").json()
[docs]
@TTLCache.cached_method(ttl="static")
def get_languages(
self, *, include_romanization: bool | None = None
) -> dict[str, Any]:
"""
`Enterprise > languages.get <https://docs.musixmatch.com
/enterprise-integration/api-reference/languages-get>`_: Get
languages supported by Musixmatch.
.. admonition:: Subscription
:class: entitlement
.. tab-set::
.. tab-item:: Required
Musixmatch Enterprise plan
Access extended music metadata, advanced search,
translations, song structure, and lyric analysis.
`Learn more. <https://about.musixmatch.com
/api-pricing>`__
Parameters
----------
include_romanization : bool; keyword-only; optional
Whether to include romanization information for romanized
languages.
**API default**: :code:`False`.
Returns
-------
languages : dict[str, Any]
Languages supported by Musixmatch.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"message": {
"body": {
"language_list": [
{
"language": {
"has_romanization": <int>,
"iso_code_romanization": <str>,
"language_iso_code_1": <str>,
"language_iso_code_3": <str>,
"language_name": <str>
}
}
]
},
"header": {
"available": <int>,
"execute_time": <float>,
"status_code": <int>
}
}
}
"""
self._client._require_api_key("enterprise.get_languages")
params = {}
if include_romanization is not None:
self._validate_type(include_romanization, bool)
if include_romanization:
params["has_romanization"] = "1"
return self._client._request(
"GET", "languages.get", params=params
).json()
[docs]
@_copy_docstring(TracksAPI.get_track_lyrics_analysis)
def get_track_lyrics_analysis(
self,
*,
track_id: int | str | None = None,
common_track_id: int | str | None = None,
isrc: str | None = None,
) -> dict[str, Any]:
return self._client.tracks.get_track_lyrics_analysis(
track_id=track_id, common_track_id=common_track_id, isrc=isrc
)