1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2013, 2014, 2015, 2021 kaliko <kaliko@azylum.org>
4 # This file is part of sima
6 # sima is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # sima is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with sima. If not, see <http://www.gnu.org/licenses/>.
21 Defines some object to handle audio file metadata
25 from collections.abc import Set
29 from ..utils.utils import MPDSimaException
32 UUID_RE = r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[89AB][a-f0-9]{3}-[a-f0-9]{12}$'
33 #: The Track Object is collapsing multiple tags into a single string using this
34 # separator. It is used then to split back the string to tags list.
35 SEPARATOR = chr(0x1F) # ASCII Unit Separator
39 """Controls MusicBrainz UUID4 format
41 :param str uuid: String representing the UUID
44 regexp = re.compile(UUID_RE, re.IGNORECASE)
45 if regexp.match(uuid):
50 class MetaException(MPDSimaException):
51 """Generic Meta Exception"""
55 def wrapper(*args, **kwargs):
58 kwargs.pop('mbid', None)
59 kwargs.pop('musicbrainz_artistid', None)
60 kwargs.pop('musicbrainz_albumartistid', None)
66 def wrapper(*args, **kwargs):
67 ans = func(*args, **kwargs)
68 if isinstance(ans, set):
69 return {s.replace("'", r"\'").replace('"', r'\"') for s in ans}
70 return ans.replace("'", r"\'").replace('"', r'\"')
76 A generic Class to handle tracks metadata such as artist, album, albumartist
77 names and their associated MusicBrainz's ID.
80 Using generic kwargs in constructor for convenience but the actual signature is:
82 >>> Meta(name, mbid=None, **kwargs)
84 :param str name: set name attribute
85 :param str mbid: set MusicBrainz ID
88 """Class attribute to disable use of MusicBrainz IDs"""
90 def __init__(self, **kwargs):
91 """Meta(name=<str>[, mbid=UUID4])"""
92 self.__name = None # TODO: should be immutable
94 self.__aliases = set()
95 self.log = logging.getLogger(__name__)
96 if 'name' not in kwargs or not kwargs.get('name'):
97 raise MetaException('Need a "name" argument (str type)')
98 if not isinstance(kwargs.get('name'), str):
99 raise MetaException('"name" argument not a string')
100 self.__name = kwargs.pop('name').split(SEPARATOR)[0]
101 if 'mbid' in kwargs and kwargs.get('mbid'):
102 mbid = kwargs.get('mbid').lower().split(SEPARATOR)[0]
106 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
107 # mbid immutable as hash rests on
108 self.__dict__.update(**kwargs)
111 fmt = '{0}(name={1.name!r}, mbid={1.mbid!r})'
112 return fmt.format(self.__class__.__name__, self)
115 return self.__name.__str__()
117 def __eq__(self, other):
119 Perform mbid equality test
121 #if hasattr(other, 'mbid'): # better isinstance?
122 if isinstance(other, Meta) and self.mbid and other.mbid:
123 return self.mbid == other.mbid
124 if isinstance(other, Meta):
125 return bool(self.names & other.names)
126 if getattr(other, '__str__', None):
127 # is other.__str__() in self.__name or self.__aliases
128 return other.__str__() in self.names
133 return hash(self.mbid)
134 return hash(self.__name)
136 def add_alias(self, other):
137 """Add alternative name to `aliases` attibute.
139 `other` can be a :class:`sima.lib.meta.Meta` object in which case aliases are merged.
141 :param str other: Alias to add, could be any object with ``__str__`` method.
143 if isinstance(other, Meta):
144 self.__aliases |= other.__aliases
145 self.__aliases -= {self.name}
146 if getattr(other, '__str__', None):
147 if callable(other.__str__) and other.__str__() != self.name:
148 self.__aliases |= {other.__str__()}
150 raise MetaException(f'No __str__ method found in {other!r}')
167 return self.__aliases
171 def aliases_sz(self):
177 return self.__aliases | {self.__name, }
189 def __init__(self, name=None, mbid=None, **kwargs):
190 if kwargs.get('musicbrainz_albumid', False):
191 mbid = kwargs.get('musicbrainz_albumid')
192 super().__init__(name=name, mbid=mbid, **kwargs)
200 """Artist object deriving from :class:`Meta`.
202 :param str name: Artist name
203 :param str mbid: Musicbrainz artist ID
204 :param str artist: Overrides "name" argument
205 :param str albumartist: use "name" if not set
206 :param str musicbrainz_artistid: Overrides "mbid" argument
210 >>> trk = {'artist':'Art Name',
211 >>> 'albumartist': 'Alb Art Name', # optional
212 >>> 'musicbrainz_artistid': '<UUID4>', # optional
214 >>> artobj0 = Artist(**trk)
215 >>> artobj1 = Artist(name='Tool')
219 def __init__(self, name=None, mbid=None, **kwargs):
220 if kwargs.get('artist', False):
221 name = kwargs.get('artist')
222 if kwargs.get('musicbrainz_artistid', False):
223 mbid = kwargs.get('musicbrainz_artistid')
224 if name and not kwargs.get('albumartist', False):
225 kwargs['albumartist'] = name.split(SEPARATOR)[0]
226 super().__init__(name=name, mbid=mbid,
227 albumartist=kwargs.get('albumartist'))
230 class MetaContainer(Set):
232 def __init__(self, iterable):
233 self.elements = lst = []
234 for value in iterable:
240 inlst.add_alias(value)
243 return iter(self.elements)
245 def __contains__(self, value):
246 return value in self.elements
249 return len(self.elements)
252 return repr(self.elements)
255 # vim: ai ts=4 sw=4 sts=4 expandtab