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"\'").replace('"', r'\"') for s in ans}
65 return ans.replace("'", r"\'").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 isinstance(other, Meta):
140 self.__aliases |= other.__aliases
141 self.__aliases -= {self.name}
142 if getattr(other, '__str__', None):
143 if callable(other.__str__) and other.__str__() != self.name:
144 self.__aliases |= {other.__str__()}
146 raise MetaException('No __str__ method found in {!r}'.format(other))
163 return self.__aliases
167 def aliases_sz(self):
173 return self.__aliases | {self.__name,}
185 def __init__(self, name=None, mbid=None, **kwargs):
186 if kwargs.get('musicbrainz_albumid', False):
187 mbid = kwargs.get('musicbrainz_albumid')
188 super().__init__(name=name, mbid=mbid, **kwargs)
196 """Artist object deriving from :class:`Meta`.
198 :param str name: Artist name
199 :param str mbid: Musicbrainz artist ID
200 :param str artist: Overrides "name" argument
201 :param str albumartist: use "name" if not set
202 :param str musicbrainz_artistid: Overrides "mbid" argument
206 >>> trk = {'artist':'Art Name',
207 >>> 'albumartist': 'Alb Art Name', # optional
208 >>> 'musicbrainz_artistid': '<UUID4>', # optional
210 >>> artobj0 = Artist(**trk)
211 >>> artobj1 = Artist(name='Tool')
215 def __init__(self, name=None, mbid=None, **kwargs):
216 if kwargs.get('artist', False):
217 name = kwargs.get('artist')
218 if kwargs.get('musicbrainz_artistid', False):
219 mbid = kwargs.get('musicbrainz_artistid')
220 if name and not kwargs.get('albumartist', False):
221 kwargs['albumartist'] = name.split(SEPARATOR)[0]
222 super().__init__(name=name, mbid=mbid,
223 albumartist=kwargs.get('albumartist'))
226 class MetaContainer(Set):
228 def __init__(self, iterable):
229 self.elements = lst = []
230 for value in iterable:
236 inlst.add_alias(value)
239 return iter(self.elements)
241 def __contains__(self, value):
242 return value in self.elements
245 return len(self.elements)
248 return repr(self.elements)
251 # vim: ai ts=4 sw=4 sts=4 expandtab