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 UUID_RE = r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[89AB][a-f0-9]{3}-[a-f0-9]{12}$'
30 #: The Track Object is collapsing multiple tags into a single string using this
31 # separator. It is used then to split back the string to tags list.
32 SEPARATOR = chr(0x1F) # ASCII Unit Separator
35 """Controls MusicBrainz UUID4 format
37 :param str uuid: String representing the UUID
40 regexp = re.compile(UUID_RE, re.IGNORECASE)
41 if regexp.match(uuid):
45 class MetaException(Exception):
46 """Generic Meta Exception"""
50 def wrapper(*args, **kwargs):
53 kwargs.pop('mbid', None)
54 kwargs.pop('musicbrainz_artistid', None)
55 kwargs.pop('musicbrainz_albumartistid', None)
61 def wrapper(*args, **kwargs):
62 ans = func(*args, **kwargs)
63 if isinstance(ans, set):
64 return {s.replace("'", r"\'") for s in ans}
65 return ans.replace("'", r"\'")
71 A generic Class to handle tracks metadata such as artist, album, albumartist
72 names and their associated MusicBrainz's ID.
75 Using generic kwargs in constructor for convenience but the actual signature is:
77 >>> Meta(name, mbid=None, **kwargs)
79 :param str name: set name attribute
80 :param str mbid: set MusicBrainz ID
83 """Class attribute to disable use of MusicBrainz IDs"""
85 def __init__(self, **kwargs):
86 """Meta(name=<str>[, mbid=UUID4])"""
87 self.__name = None # TODO: should be immutable
89 self.__aliases = set()
90 self.log = logging.getLogger(__name__)
91 if 'name' not in kwargs or not kwargs.get('name'):
92 raise MetaException('Need a "name" argument (str type)')
93 if not isinstance(kwargs.get('name'), str):
94 raise MetaException('"name" argument not a string')
96 self.__name = kwargs.pop('name').split(SEPARATOR)[0]
97 if 'mbid' in kwargs and kwargs.get('mbid'):
98 mbid = kwargs.get('mbid').lower().split(SEPARATOR)[0]
102 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
103 # mbid immutable as hash rests on
104 self.__dict__.update(**kwargs)
107 fmt = '{0}(name={1.name!r}, mbid={1.mbid!r})'
108 return fmt.format(self.__class__.__name__, self)
111 return self.__name.__str__()
113 def __eq__(self, other):
115 Perform mbid equality test
117 #if hasattr(other, 'mbid'): # better isinstance?
118 if isinstance(other, Meta) and self.mbid and other.mbid:
119 return self.mbid == other.mbid
120 if isinstance(other, Meta):
121 return bool(self.names & other.names)
122 if getattr(other, '__str__', None):
123 # is other.__str__() in self.__name or self.__aliases
124 return other.__str__() in self.names
129 return hash(self.mbid)
130 return hash(self.__name)
132 def add_alias(self, other):
133 """Add alternative name to `aliases` attibute.
135 `other` can be a :class:`sima.lib.meta.Meta` object in which case aliases are merged.
137 :param str other: Alias to add, could be any object with ``__str__`` method.
139 if getattr(other, '__str__', None):
140 if callable(other.__str__) and other.__str__() != self.name:
141 self.__aliases |= {other.__str__()}
142 elif isinstance(other, Meta):
143 if other.name != self.name:
144 self.__aliases |= other.__aliases
146 raise MetaException('No __str__ method found in {!r}'.format(other))
162 def mbid(self, mbid):
163 if mbid and not is_uuid4(mbid):
164 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
170 return self.__aliases
174 def aliases_sz(self):
180 return self.__aliases | {self.__name,}
192 def __init__(self, name=None, mbid=None, **kwargs):
193 if kwargs.get('musicbrainz_albumid', False):
194 mbid = kwargs.get('musicbrainz_albumid')
195 super().__init__(name=name, mbid=mbid, **kwargs)
203 """Artist object deriving from :class:`Meta`.
205 :param str name: Artist name
206 :param str mbid: Musicbrainz artist ID
207 :param str artist: Overrides "name" argument
208 :param str albumartist: use "name" if not set
209 :param str musicbrainz_artistid: Overrides "mbid" argument
213 >>> trk = {'artist':'Art Name',
214 >>> 'albumartist': 'Alb Art Name', # optional
215 >>> 'musicbrainz_artistid': '<UUID4>', # optional
217 >>> artobj0 = Artist(**trk)
218 >>> artobj1 = Artist(name='Tool')
222 def __init__(self, name=None, mbid=None, **kwargs):
223 if kwargs.get('artist', False):
224 name = kwargs.get('artist')
225 if kwargs.get('musicbrainz_artistid', False):
226 mbid = kwargs.get('musicbrainz_artistid')
227 if name and not kwargs.get('albumartist', False):
228 kwargs['albumartist'] = name.split(SEPARATOR)[0]
229 super().__init__(name=name, mbid=mbid,
230 albumartist=kwargs.get('albumartist'))
233 class MetaContainer(Set):
235 def __init__(self, iterable):
236 self.elements = lst = []
237 for value in iterable:
243 inlst.add_alias(value)
246 return iter(self.elements)
248 def __contains__(self, value):
249 return value in self.elements
252 return len(self.elements)
255 return repr(self.elements)
258 # vim: ai ts=4 sw=4 sts=4 expandtab