Source code for mscxyz.meta
"""Class for metadata maniplation"""
from __future__ import annotations
import typing
import tmep
from lxml.etree import _Element
if typing.TYPE_CHECKING:
from mscxyz.score import Score
[docs]
class UnmatchedFormatStringError(Exception):
def __init__(self, format_string: str, input_string: str) -> None:
self.msg = (
"Your format string “{}” doesn’t match on this "
"input string: “{}”".format(format_string, input_string)
)
Exception.__init__(self, self.msg)
[docs]
class FormatStringNoFieldError(Exception):
def __init__(self, format_string: str) -> None:
self.msg = "No fields found in your format string “{}”!".format(format_string)
Exception.__init__(self, self.msg)
[docs]
class Metatag:
"""
The class provides access to the MuseScore metadata fields.
The class should not be renamed to ``MetaTag`` because it would conflict with the
naming scheme of the fields ``metatag_title`` etc.
:see: `MuseScore Handbook <https://musescore.org/en/handbook/4/project-properties>`_
The available ``metaTag`` fields are:
* `arranger`
* `audioComUrl` (new in v4)
* `composer`
* `copyright`
* `creationDate`
* `lyricist`
* `movementNumber`
* `movementTitle`
* `mscVersion`
* `platform`
* `poet` (not in v4)
* `source`
* `sourceRevisionId`
* `subtitle`
* `translator`
* `workNumber`
* `workTitle`
version 4
.. code-block:: xml
<museScore version="4.20">
<Score>
<metaTag name="arranger"></metaTag>
<metaTag name="audioComUrl"></metaTag>
<metaTag name="composer">Composer / arranger</metaTag>
<metaTag name="copyright"></metaTag>
<metaTag name="creationDate">2024-01-05</metaTag>
<metaTag name="lyricist"></metaTag>
<metaTag name="movementNumber"></metaTag>
<metaTag name="movementTitle"></metaTag>
<metaTag name="platform">Linux</metaTag>
<metaTag name="source"></metaTag>
<metaTag name="sourceRevisionId"></metaTag>
<metaTag name="subtitle">Subtitle</metaTag>
<metaTag name="translator"></metaTag>
<metaTag name="workNumber"></metaTag>
<metaTag name="workTitle">Untitled score</metaTag>
"""
fields = (
"arranger",
"audio_com_url",
"composer",
"copyright",
"creation_date",
"lyricist",
"movement_number",
"movement_title",
"msc_version",
"platform",
"poet",
"source",
"source_revision_id",
"subtitle",
"translator",
"work_number",
"work_title",
)
score: "Score"
xml_root: _Element
def __init__(self, score: "Score") -> None:
self.score = score
self.xml_root = score.xml_root
def __get_element(self, field: str) -> _Element:
score_element: _Element = self.score.xml.find_safe("Score")
element: _Element | None = self.score.xml.xpath(
'//metaTag[@name="' + field + '"]'
)
if element is None:
_, element = self.score.xml.create_sub_element(
score_element, "metaTag", "", attrib={"name": field}
)
return element
def __get_text(self, field: str) -> str | None:
element: _Element | None = self.__get_element(field)
return self.score.xml.get_text(element)
def __set_text(self, field: str, value: str | None) -> None:
element: _Element = self.__get_element(field)
element.text = value
@property
def arranger(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="arranger">...</metaTag>
"""
return self.__get_text("arranger")
@arranger.setter
def arranger(self, value: str | None) -> None:
self.__set_text("arranger", value)
@property
def audio_com_url(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="audioComUrl">...</metaTag>
"""
return self.__get_text("audioComUrl")
@audio_com_url.setter
def audio_com_url(self, value: str | None) -> None:
self.__set_text("audioComUrl", value)
@property
def composer(self) -> str | None:
"""Same text as "Composer" on the first page of the score
.. code-block:: xml
<metaTag name="composer">...</metaTag>
"""
return self.__get_text("composer")
@composer.setter
def composer(self, value: str | None) -> None:
self.__set_text("composer", value)
@property
def copyright(self) -> str | None:
"""Same text as "Copyright" on the first page of the score.
.. code-block:: xml
<metaTag name="copyright">...</metaTag>
"""
return self.__get_text("copyright")
@copyright.setter
def copyright(self, value: str | None) -> None:
self.__set_text("copyright", value)
@property
def creation_date(self) -> str | None:
"""
https://github.com/musescore/MuseScore/blob/06793ff5ff3065fe87fe9a8a651a6d20f49fd28c/src/engraving/dom/masterscore.cpp#L93
.. code-block:: xml
<metaTag name="creationDate">2024-01-05</metaTag>
"""
return self.__get_text("creationDate")
@creation_date.setter
def creation_date(self, value: str | None) -> None:
self.__set_text("creationDate", value)
@property
def lyricist(self) -> str | None:
"""Same text as “Lyricist” on the first page of the score.
.. code-block:: xml
<metaTag name="lyricist">...</metaTag>
"""
return self.__get_text("lyricist")
@lyricist.setter
def lyricist(self, value: str | None) -> None:
self.__set_text("lyricist", value)
@property
def movement_number(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="movementNumber">...</metaTag>
"""
return self.__get_text("movementNumber")
@movement_number.setter
def movement_number(self, value: str | None) -> None:
self.__set_text("movementNumber", value)
@property
def movement_title(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="movementTitle">...</metaTag>
"""
return self.__get_text("movementTitle")
@movement_title.setter
def movement_title(self, value: str | None) -> None:
self.__set_text("movementTitle", value)
@property
def msc_version(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="mscVersion">4.20</metaTag>
"""
return self.__get_text("mscVersion")
@msc_version.setter
def msc_version(self, value: str | None) -> None:
self.__set_text("mscVersion", value)
@property
def platform(self) -> str | None:
"""The computing platform the score was created on. This might be empty if the score was saved in test mode.
https://github.com/musescore/MuseScore/blob/06793ff5ff3065fe87fe9a8a651a6d20f49fd28c/src/engraving/dom/masterscore.cpp#L74-L81
.. code-block:: xml
<metaTag name="platform">Linux</metaTag>
<metaTag name="platform">Apple Macintosh</metaTag>
"""
return self.__get_text("platform")
@platform.setter
def platform(self, value: str | None) -> None:
self.__set_text("platform", value)
@property
def poet(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="poet">...</metaTag>
"""
return self.__get_text("poet")
@poet.setter
def poet(self, value: str | None) -> None:
self.__set_text("poet", value)
@property
def source(self) -> str | None:
"""May contain a URL if the score was downloaded from or Publish to MuseScore.com.
.. code-block:: xml
<metaTag name="source">http://musescore.com/isaacweiss/getting-started</metaTag>
<metaTag name="source">http://musescore.com/score/111410</metaTag>
"""
return self.__get_text("source")
@source.setter
def source(self, value: str | None) -> None:
self.__set_text("source", value)
@property
def source_revision_id(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="sourceRevisionId">...</metaTag>
"""
return self.__get_text("sourceRevisionId")
@source_revision_id.setter
def source_revision_id(self, value: str | None) -> None:
self.__set_text("sourceRevisionId", value)
@property
def subtitle(self) -> str | None:
"""
The subtitle. It has the same text as “Subtitle” on the first page of the score.
.. code-block:: xml
<metaTag name="subtitle">Subtitle</metaTag>
"""
return self.__get_text("subtitle")
@subtitle.setter
def subtitle(self, value: str | None) -> None:
self.__set_text("subtitle", value)
@property
def translator(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="translator">...</metaTag>
"""
return self.__get_text("translator")
@translator.setter
def translator(self, value: str | None) -> None:
self.__set_text("translator", value)
@property
def work_number(self) -> str | None:
"""
.. code-block:: xml
<metaTag name="workNumber">...</metaTag>
"""
return self.__get_text("workNumber")
@work_number.setter
def work_number(self, value: str | None) -> None:
self.__set_text("workNumber", value)
@property
def work_title(self) -> str | None:
"""
The Work Title. It has the same text as “Title” on the first page of the score.
.. code-block:: xml
<metaTag name="workTitle">Untitled score</metaTag>
"""
return self.__get_text("workTitle")
@work_title.setter
def work_title(self, value: str | None) -> None:
self.__set_text("workTitle", value)
[docs]
class Vbox:
"""The first `vertical` box or frame of a score.
Available fields:
* `Composer`
* `Lyricist`
* `Subtitle`
* `Title`
Version 2, 3
.. code-block:: xml
<Staff id="1">
<VBox>
<height>10</height>
<Text>
<style>Title</style>
<text>Title</text>
</Text>
<Text>
<style>Composer</style>
<text>Composer</text>
</Text>
</VBox>
</Staff>
Version 4
.. code-block:: xml
<Staff id="1">
<VBox>
<height>10</height>
<boxAutoSize>0</boxAutoSize>
<eid>4294967418</eid>
<Text>
<eid>8589934598</eid>
<style>title</style>
<text>Title</text>
</Text>
<Text>
<eid>12884901894</eid>
<style>composer</style>
<text>Composer</text>
</Text>
</VBox>
</Staff>
"""
fields = (
"composer",
"lyricist",
"subtitle",
"title",
)
score: "Score"
xml_root: _Element
vbox: _Element
def __init__(self, score: "Score") -> None:
self.score = score
self.xml_root = score.xml_root
xpath = '/museScore/Score/Staff[@id="1"]'
vbox = self.score.xml.xpath(xpath + "/VBox")
if vbox is None:
vbox, _ = self.score.xml.create_sub_element("VBox", "height", "10")
self.score.xml.xpath_safe(xpath).insert(0, vbox)
self.vbox = vbox
def __normalize_style_name(self, style: str) -> str:
"""
:param style: The string inside the ``<style>`` tags, for example
``Title`` or ``Composer`` or for v4 ``title`` or ``composer``.
"""
if self.score.version_major in (2, 3):
style = style.title()
elif self.score.version_major == 4:
style = style.lower()
return style
def __get_element(self, style: str) -> _Element | None:
"""
:param style: The string inside the ``<style>`` tags, for example
``Title`` or ``Composer`` or for v4 ``title`` or ``composer``.
"""
for element in self.vbox:
s: _Element | None = element.find("style")
if s is not None and s.text == self.__normalize_style_name(style):
return element.find("text")
return None
def __get_text(self, style: str) -> str | None:
"""
:param style: The string inside the ``<style>`` tags, for example
``Title`` or ``Composer``.
"""
element: _Element | None = self.__get_element(style)
if element is not None and hasattr(element, "text"):
return element.text
return None
def __create_text_element(self, style: str, text: str) -> None:
"""
Version 2, 3
.. code-block:: xml
<Text>
<style>Title</style>
<text>Title</text>
</Text>
Version 4
.. code-block:: xml
<Text>
<eid>8589934598</eid>
<style>title</style>
<text>Title</text>
</Text>
:param style: The string inside the ``<style>`` tags, for example
``Title`` or ``Composer`` or for v4 ``title`` or ``composer``.
:param text: The string inside the ``<text>`` tags.
"""
text_element: _Element = self.score.xml.create_element("Text")
if self.score.version_major in (2, 3):
style = style.title()
else:
style = style.lower()
self.score.xml.create_sub_element(
text_element, "style", self.__normalize_style_name(style)
)
self.score.xml.create_sub_element(text_element, "text", text)
self.vbox.append(text_element)
def __set_text(self, style: str, text: str | None) -> None:
"""
:param style: The string inside the ``<style>`` tags, for example
``Title`` or ``Composer`` or for v4 ``title`` or ``composer``.
"""
if text is None:
self.__remove_text_element(style)
return None
element: _Element | None = self.__get_element(style)
if hasattr(element, "text"):
if element is not None:
element.text = text
else:
self.__create_text_element(style, text)
def __remove_text_element(self, style: str) -> None:
"""
:param style: The string inside the ``<style>`` tags, for example
``Title`` or ``Composer`` or for v4 ``title`` or ``composer``.
"""
self.score.xml.remove(self.__get_element(style))
return None
@property
def title(self) -> str | None:
"""
The title text field of the first `vertical` box or frame of a score.
.. code-block:: xml
<Staff id="1">
<VBox>
<height>10</height>
<boxAutoSize>0</boxAutoSize>
<eid>4294967418</eid>
<Text>
<eid>8589934598</eid>
<style>title</style>
<text>Mondscheinsonate</text>
</Text>
</VBox>
</Staff>
"""
return self.__get_text("title")
@title.setter
def title(self, value: str | None) -> None:
self.__set_text("title", value)
@property
def subtitle(self) -> str | None:
"""
The subtitle text field of the first `vertical` box or frame of a score.
.. code-block:: xml
<Staff id="1">
<VBox>
<height>10</height>
<boxAutoSize>0</boxAutoSize>
<eid>4294967418</eid>
<Text>
<eid>8589934598</eid>
<style>subtitle</style>
<text>1. Satz</text>
</Text>
</VBox>
</Staff>
"""
return self.__get_text("subtitle")
@subtitle.setter
def subtitle(self, value: str | None) -> None:
self.__set_text("subtitle", value)
@property
def composer(self) -> str | None:
"""
The composer text field of the first `vertical` box or frame of a score.
.. code-block:: xml
<Staff id="1">
<VBox>
<height>10</height>
<boxAutoSize>0</boxAutoSize>
<eid>4294967418</eid>
<Text>
<eid>8589934598</eid>
<style>composer</style>
<text>Ludwig van Beethoven</text>
</Text>
</VBox>
</Staff>
"""
return self.__get_text("composer")
@composer.setter
def composer(self, value: str | None) -> None:
self.__set_text("composer", value)
@property
def lyricist(self) -> str | None:
"""
The composer text field of the first `vertical` box or frame of a score.
.. code-block:: xml
<Staff id="1">
<VBox>
<height>10</height>
<boxAutoSize>0</boxAutoSize>
<eid>4294967418</eid>
<Text>
<eid>8589934598</eid>
<style>lyricist</style>
<text>Johann Wolfgang von Goethe</text>
</Text>
</VBox>
</Staff>
"""
return self.__get_text("lyricist")
@lyricist.setter
def lyricist(self, value: str | None) -> None:
self.__set_text("lyricist", value)
[docs]
class Meta:
score: "Score"
metatag: Metatag
vbox: Vbox
def __init__(self, score: "Score") -> None:
self.score = score
self.metatag = Metatag(self.score)
self.vbox = Vbox(self.score)
[docs]
def sync_fields(self) -> None:
self.title = self.title
self.subtitle = self.subtitle
self.composer = self.composer
self.lyricist = self.lyricist
[docs]
def write_to_log_file(self, log_file: str, format_string: str) -> None:
log = open(log_file, "w")
log.write(tmep.parse(format_string, self.score.fields.export_to_dict()) + "\n")
log.close()
[docs]
def clean(self) -> None:
"""
Clean all metadata fields of the object.
"""
self.metatag.clean()
self.vbox.clean()
[docs]
def delete_duplicates(self) -> None:
"""
Delete duplicates in the metadata.
This method checks if the :attr:`lyricist` and :attr:`composer` are the same,
and if so, it sets :attr:`lyricist` to an empty string.
It also checks if :attr:`title` is empty but :attr:`subtitle` is not,
and if so, it sets :attr:`title` to :attr:`subtitle`.
Finally, it checks if :attr:`subtitle` is the same as :attr:`title`,
and if so, it sets :attr:`subtitle` to an empty string.
"""
if self.lyricist == self.composer:
self.lyricist = None
if not self.title and self.subtitle:
self.title = self.subtitle
if self.subtitle == self.title:
self.subtitle = None
[docs]
def reload(self, save: bool = False) -> Meta:
"""
Reload the MuseScore file.
:param save: Whether to save the changes before reloading. Default is False.
:return: The reloaded Meta object.
:see: :meth:`mscxyz.Score.reload`
"""
return self.score.reload(save).meta
def __pick_value(self, *values: str | None) -> str | None:
for value in values:
if value:
return value
return None
@property
def title(self) -> str | None:
"""
Get and set the value of :attr:`Vbox.title` and :attr:`Metatag.work_title` all at once.
If the attributes have different values, then the attribute :attr:`Vbox.title` is preferred.
"""
return self.__pick_value(self.vbox.title, self.metatag.work_title)
@title.setter
def title(self, value: str | None) -> None:
self.vbox.title = self.metatag.work_title = value
@property
def subtitle(self) -> str | None:
"""
Get and set the value of :attr:`Vbox.subtitle`, :attr:`Metatag.subtitle` and :attr:`Metatag.movement_title` all at once.
If the attributes have different values, then the attribute :attr:`Vbox.subtitle` is preferred.
"""
return self.__pick_value(
self.vbox.subtitle, self.metatag.subtitle, self.metatag.movement_title
)
@subtitle.setter
def subtitle(self, value: str | None) -> None:
self.vbox.subtitle = self.metatag.subtitle = self.metatag.movement_title = value
@property
def composer(self) -> str | None:
"""
Get and set the value of :attr:`Vbox.composer` and :attr:`Metatag.composer` all at once.
If the attributes have different values, then the attribute :attr:`Vbox.composer` is preferred.
"""
return self.__pick_value(self.vbox.composer, self.metatag.composer)
@composer.setter
def composer(self, value: str | None) -> None:
self.vbox.composer = self.metatag.composer = value
@property
def lyricist(self) -> str | None:
"""
Get and set the value of :attr:`Vbox.lyricist` and :attr:`Metatag.lyricist` all at once.
If the attributes have different values, then the attribute :attr:`Vbox.lyricist` is preferred.
"""
return self.__pick_value(self.vbox.lyricist, self.metatag.lyricist)
@lyricist.setter
def lyricist(self, value: str | None) -> None:
self.vbox.lyricist = self.metatag.lyricist = value