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
36 """Controls MusicBrainz UUID4 format
38 :param str uuid: String representing the UUID
41 regexp = re.compile(UUID_RE, re.IGNORECASE)
42 if regexp.match(uuid):
47 class MetaException(Exception):
48 """Generic Meta Exception"""
52 def wrapper(*args, **kwargs):
55 kwargs.pop('mbid', None)
56 kwargs.pop('musicbrainz_artistid', None)
57 kwargs.pop('musicbrainz_albumartistid', None)
63 def wrapper(*args, **kwargs):
64 ans = func(*args, **kwargs)
65 if isinstance(ans, set):
66 return {s.replace("'", r"\'").replace('"', r'\"') for s in ans}
67 return ans.replace("'", r"\'").replace('"', r'\"')
73 A generic Class to handle tracks metadata such as artist, album, albumartist
74 names and their associated MusicBrainz's ID.
77 Using generic kwargs in constructor for convenience but the actual signature is:
79 >>> Meta(name, mbid=None, **kwargs)
81 :param str name: set name attribute
82 :param str mbid: set MusicBrainz ID
85 """Class attribute to disable use of MusicBrainz IDs"""
87 def __init__(self, **kwargs):
88 """Meta(name=<str>[, mbid=UUID4])"""
89 self.__name = None # TODO: should be immutable
91 self.__aliases = set()
92 self.log = logging.getLogger(__name__)
93 if 'name' not in kwargs or not kwargs.get('name'):
94 raise MetaException('Need a "name" argument (str type)')
95 if not isinstance(kwargs.get('name'), str):
96 raise MetaException('"name" argument not a string')
98 self.__name = kwargs.pop('name').split(SEPARATOR)[0]
99 if 'mbid' in kwargs and kwargs.get('mbid'):
100 mbid = kwargs.get('mbid').lower().split(SEPARATOR)[0]
104 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
105 # mbid immutable as hash rests on
106 self.__dict__.update(**kwargs)
109 fmt = '{0}(name={1.name!r}, mbid={1.mbid!r})'
110 return fmt.format(self.__class__.__name__, self)
113 return self.__name.__str__()
115 def __eq__(self, other):
117 Perform mbid equality test
119 #if hasattr(other, 'mbid'): # better isinstance?
120 if isinstance(other, Meta) and self.mbid and other.mbid:
121 return self.mbid == other.mbid
122 if isinstance(other, Meta):
123 return bool(self.names & other.names)
124 if getattr(other, '__str__', None):
125 # is other.__str__() in self.__name or self.__aliases
126 return other.__str__() in self.names
131 return hash(self.mbid)
132 return hash(self.__name)
134 def add_alias(self, other):
135 """Add alternative name to `aliases` attibute.
137 `other` can be a :class:`sima.lib.meta.Meta` object in which case aliases are merged.
139 :param str other: Alias to add, could be any object with ``__str__`` method.
141 if isinstance(other, Meta):
142 self.__aliases |= other.__aliases
143 self.__aliases -= {self.name}
144 if getattr(other, '__str__', None):
145 if callable(other.__str__) and other.__str__() != self.name:
146 self.__aliases |= {other.__str__()}
148 raise MetaException('No __str__ method found in {!r}'.format(other))
165 return self.__aliases
169 def aliases_sz(self):
175 return self.__aliases | {self.__name, }
187 def __init__(self, name=None, mbid=None, **kwargs):
188 if kwargs.get('musicbrainz_albumid', False):
189 mbid = kwargs.get('musicbrainz_albumid')
190 super().__init__(name=name, mbid=mbid, **kwargs)
198 """Artist object deriving from :class:`Meta`.
200 :param str name: Artist name
201 :param str mbid: Musicbrainz artist ID
202 :param str artist: Overrides "name" argument
203 :param str albumartist: use "name" if not set
204 :param str musicbrainz_artistid: Overrides "mbid" argument
208 >>> trk = {'artist':'Art Name',
209 >>> 'albumartist': 'Alb Art Name', # optional
210 >>> 'musicbrainz_artistid': '<UUID4>', # optional
212 >>> artobj0 = Artist(**trk)
213 >>> artobj1 = Artist(name='Tool')
217 def __init__(self, name=None, mbid=None, **kwargs):
218 if kwargs.get('artist', False):
219 name = kwargs.get('artist')
220 if kwargs.get('musicbrainz_artistid', False):
221 mbid = kwargs.get('musicbrainz_artistid')
222 if name and not kwargs.get('albumartist', False):
223 kwargs['albumartist'] = name.split(SEPARATOR)[0]
224 super().__init__(name=name, mbid=mbid,
225 albumartist=kwargs.get('albumartist'))
228 class MetaContainer(Set):
230 def __init__(self, iterable):
231 self.elements = lst = []
232 for value in iterable:
238 inlst.add_alias(value)
241 return iter(self.elements)
243 def __contains__(self, value):
244 return value in self.elements
247 return len(self.elements)
250 return repr(self.elements)
253 # vim: ai ts=4 sw=4 sts=4 expandtab