]> kaliko git repositories - mpd-sima.git/blob - sima/lib/meta.py
Some refactoring around Exceptions
[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 from ..utils.utils import MPDSimaException
30
31
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
36
37
38 def is_uuid4(uuid):
39     """Controls MusicBrainz UUID4 format
40
41     :param str uuid: String representing the UUID
42     :returns: boolean
43     """
44     regexp = re.compile(UUID_RE, re.IGNORECASE)
45     if regexp.match(uuid):
46         return True
47     return False
48
49
50 class MetaException(MPDSimaException):
51     """Generic Meta Exception"""
52
53
54 def mbidfilter(func):
55     def wrapper(*args, **kwargs):
56         cls = args[0]
57         if not cls.use_mbid:
58             kwargs.pop('mbid', None)
59             kwargs.pop('musicbrainz_artistid', None)
60             kwargs.pop('musicbrainz_albumartistid', None)
61         func(*args, **kwargs)
62     return wrapper
63
64
65 def serialize(func):
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'\"')
71     return wrapper
72
73
74 class Meta:
75     """
76     A generic Class to handle tracks metadata such as artist, album, albumartist
77     names and their associated MusicBrainz's ID.
78
79
80     Using generic kwargs in constructor for convenience but the actual signature is:
81
82     >>> Meta(name, mbid=None, **kwargs)
83
84     :param str name: set name attribute
85     :param str mbid: set MusicBrainz ID
86     """
87     use_mbid = True
88     """Class attribute to disable use of MusicBrainz IDs"""
89
90     def __init__(self, **kwargs):
91         """Meta(name=<str>[, mbid=UUID4])"""
92         self.__name = None  # TODO: should be immutable
93         self.__mbid = None
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         else:
101             self.__name = kwargs.pop('name').split(SEPARATOR)[0]
102         if 'mbid' in kwargs and kwargs.get('mbid'):
103             mbid = kwargs.get('mbid').lower().split(SEPARATOR)[0]
104             if is_uuid4(mbid):
105                 self.__mbid = mbid
106             else:
107                 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
108             # mbid immutable as hash rests on
109         self.__dict__.update(**kwargs)
110
111     def __repr__(self):
112         fmt = '{0}(name={1.name!r}, mbid={1.mbid!r})'
113         return fmt.format(self.__class__.__name__, self)
114
115     def __str__(self):
116         return self.__name.__str__()
117
118     def __eq__(self, other):
119         """
120         Perform mbid equality test
121         """
122         #if hasattr(other, 'mbid'):  # better isinstance?
123         if isinstance(other, Meta) and self.mbid and other.mbid:
124             return self.mbid == other.mbid
125         if isinstance(other, Meta):
126             return bool(self.names & other.names)
127         if getattr(other, '__str__', None):
128             # is other.__str__() in self.__name or self.__aliases
129             return other.__str__() in self.names
130         return False
131
132     def __hash__(self):
133         if self.mbid:
134             return hash(self.mbid)
135         return hash(self.__name)
136
137     def add_alias(self, other):
138         """Add alternative name to `aliases` attibute.
139
140         `other` can be a :class:`sima.lib.meta.Meta` object in which case aliases are merged.
141
142         :param str other: Alias to add, could be any object with ``__str__`` method.
143         """
144         if isinstance(other, Meta):
145             self.__aliases |= other.__aliases
146             self.__aliases -= {self.name}
147         if getattr(other, '__str__', None):
148             if callable(other.__str__) and other.__str__() != self.name:
149                 self.__aliases |= {other.__str__()}
150         else:
151             raise MetaException('No __str__ method found in {!r}'.format(other))
152
153     @property
154     def name(self):
155         return self.__name
156
157     @property
158     @serialize
159     def name_sz(self):
160         return self.name
161
162     @property
163     def mbid(self):
164         return self.__mbid
165
166     @property
167     def aliases(self):
168         return self.__aliases
169
170     @property
171     @serialize
172     def aliases_sz(self):
173         return self.aliases
174
175     @property
176     def names(self):
177         """aliases + name"""
178         return self.__aliases | {self.__name, }
179
180     @property
181     @serialize
182     def names_sz(self):
183         return self.names
184
185
186 class Album(Meta):
187     """Album object"""
188
189     @mbidfilter
190     def __init__(self, name=None, mbid=None, **kwargs):
191         if kwargs.get('musicbrainz_albumid', False):
192             mbid = kwargs.get('musicbrainz_albumid')
193         super().__init__(name=name, mbid=mbid, **kwargs)
194
195     @property
196     def album(self):
197         return self.name
198
199
200 class Artist(Meta):
201     """Artist object deriving from :class:`Meta`.
202
203     :param str name: Artist name
204     :param str mbid: Musicbrainz artist ID
205     :param str artist: Overrides "name" argument
206     :param str albumartist: use "name" if not set
207     :param str musicbrainz_artistid: Overrides "mbid" argument
208
209     :Example:
210
211     >>> trk = {'artist':'Art Name',
212     >>>        'albumartist': 'Alb Art Name',           # optional
213     >>>        'musicbrainz_artistid': '<UUID4>',       # optional
214     >>>       }
215     >>> artobj0 = Artist(**trk)
216     >>> artobj1 = Artist(name='Tool')
217     """
218
219     @mbidfilter
220     def __init__(self, name=None, mbid=None, **kwargs):
221         if kwargs.get('artist', False):
222             name = kwargs.get('artist')
223         if kwargs.get('musicbrainz_artistid', False):
224             mbid = kwargs.get('musicbrainz_artistid')
225         if name and not kwargs.get('albumartist', False):
226             kwargs['albumartist'] = name.split(SEPARATOR)[0]
227         super().__init__(name=name, mbid=mbid,
228                          albumartist=kwargs.get('albumartist'))
229
230
231 class MetaContainer(Set):
232
233     def __init__(self, iterable):
234         self.elements = lst = []
235         for value in iterable:
236             if value not in lst:
237                 lst.append(value)
238             else:
239                 for inlst in lst:
240                     if value == inlst:
241                         inlst.add_alias(value)
242
243     def __iter__(self):
244         return iter(self.elements)
245
246     def __contains__(self, value):
247         return value in self.elements
248
249     def __len__(self):
250         return len(self.elements)
251
252     def __repr__(self):
253         return repr(self.elements)
254
255 # VIM MODLINE
256 # vim: ai ts=4 sw=4 sts=4 expandtab