]> kaliko git repositories - mpd-sima.git/blob - sima/lib/meta.py
Clean some linter warnings and errors
[mpd-sima.git] / sima / lib / meta.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2013, 2014, 2015, 2021 kaliko <kaliko@azylum.org>
3 #
4 #  This file is part of sima
5 #
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.
10 #
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.
15 #
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/>.
18 #
19 #
20 """
21 Defines some object to handle audio file metadata
22 """
23
24
25 from collections.abc import Set
26 import logging
27 import re
28
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
33
34
35 def is_uuid4(uuid):
36     """Controls MusicBrainz UUID4 format
37
38     :param str uuid: String representing the UUID
39     :returns: boolean
40     """
41     regexp = re.compile(UUID_RE, re.IGNORECASE)
42     if regexp.match(uuid):
43         return True
44     return False
45
46
47 class MetaException(Exception):
48     """Generic Meta Exception"""
49
50
51 def mbidfilter(func):
52     def wrapper(*args, **kwargs):
53         cls = args[0]
54         if not cls.use_mbid:
55             kwargs.pop('mbid', None)
56             kwargs.pop('musicbrainz_artistid', None)
57             kwargs.pop('musicbrainz_albumartistid', None)
58         func(*args, **kwargs)
59     return wrapper
60
61
62 def serialize(func):
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'\"')
68     return wrapper
69
70
71 class Meta:
72     """
73     A generic Class to handle tracks metadata such as artist, album, albumartist
74     names and their associated MusicBrainz's ID.
75
76
77     Using generic kwargs in constructor for convenience but the actual signature is:
78
79     >>> Meta(name, mbid=None, **kwargs)
80
81     :param str name: set name attribute
82     :param str mbid: set MusicBrainz ID
83     """
84     use_mbid = True
85     """Class attribute to disable use of MusicBrainz IDs"""
86
87     def __init__(self, **kwargs):
88         """Meta(name=<str>[, mbid=UUID4])"""
89         self.__name = None  # TODO: should be immutable
90         self.__mbid = None
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')
97         else:
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]
101             if is_uuid4(mbid):
102                 self.__mbid = mbid
103             else:
104                 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
105             # mbid immutable as hash rests on
106         self.__dict__.update(**kwargs)
107
108     def __repr__(self):
109         fmt = '{0}(name={1.name!r}, mbid={1.mbid!r})'
110         return fmt.format(self.__class__.__name__, self)
111
112     def __str__(self):
113         return self.__name.__str__()
114
115     def __eq__(self, other):
116         """
117         Perform mbid equality test
118         """
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
127         return False
128
129     def __hash__(self):
130         if self.mbid:
131             return hash(self.mbid)
132         return hash(self.__name)
133
134     def add_alias(self, other):
135         """Add alternative name to `aliases` attibute.
136
137         `other` can be a :class:`sima.lib.meta.Meta` object in which case aliases are merged.
138
139         :param str other: Alias to add, could be any object with ``__str__`` method.
140         """
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__()}
147         else:
148             raise MetaException('No __str__ method found in {!r}'.format(other))
149
150     @property
151     def name(self):
152         return self.__name
153
154     @property
155     @serialize
156     def name_sz(self):
157         return self.name
158
159     @property
160     def mbid(self):
161         return self.__mbid
162
163     @property
164     def aliases(self):
165         return self.__aliases
166
167     @property
168     @serialize
169     def aliases_sz(self):
170         return self.aliases
171
172     @property
173     def names(self):
174         """aliases + name"""
175         return self.__aliases | {self.__name, }
176
177     @property
178     @serialize
179     def names_sz(self):
180         return self.names
181
182
183 class Album(Meta):
184     """Album object"""
185
186     @mbidfilter
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)
191
192     @property
193     def album(self):
194         return self.name
195
196
197 class Artist(Meta):
198     """Artist object deriving from :class:`Meta`.
199
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
205
206     :Example:
207
208     >>> trk = {'artist':'Art Name',
209     >>>        'albumartist': 'Alb Art Name',           # optional
210     >>>        'musicbrainz_artistid': '<UUID4>',       # optional
211     >>>       }
212     >>> artobj0 = Artist(**trk)
213     >>> artobj1 = Artist(name='Tool')
214     """
215
216     @mbidfilter
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'))
226
227
228 class MetaContainer(Set):
229
230     def __init__(self, iterable):
231         self.elements = lst = []
232         for value in iterable:
233             if value not in lst:
234                 lst.append(value)
235             else:
236                 for inlst in lst:
237                     if value == inlst:
238                         inlst.add_alias(value)
239
240     def __iter__(self):
241         return iter(self.elements)
242
243     def __contains__(self, value):
244         return value in self.elements
245
246     def __len__(self):
247         return len(self.elements)
248
249     def __repr__(self):
250         return repr(self.elements)
251
252 # VIM MODLINE
253 # vim: ai ts=4 sw=4 sts=4 expandtab