]> kaliko git repositories - mpd-sima.git/blob - sima/lib/meta.py
Bump version
[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         self.__name = kwargs.pop('name').split(SEPARATOR)[0]
101         if 'mbid' in kwargs and kwargs.get('mbid'):
102             mbid = kwargs.get('mbid').lower().split(SEPARATOR)[0]
103             if is_uuid4(mbid):
104                 self.__mbid = mbid
105             else:
106                 self.log.warning('Wrong mbid %s:%s', self.__name, mbid)
107             # mbid immutable as hash rests on
108         self.__dict__.update(**kwargs)
109
110     def __repr__(self):
111         fmt = '{0}(name={1.name!r}, mbid={1.mbid!r})'
112         return fmt.format(self.__class__.__name__, self)
113
114     def __str__(self):
115         return self.__name.__str__()
116
117     def __eq__(self, other):
118         """
119         Perform mbid equality test
120         """
121         #if hasattr(other, 'mbid'):  # better isinstance?
122         if isinstance(other, Meta) and self.mbid and other.mbid:
123             return self.mbid == other.mbid
124         if isinstance(other, Meta):
125             return bool(self.names & other.names)
126         if getattr(other, '__str__', None):
127             # is other.__str__() in self.__name or self.__aliases
128             return other.__str__() in self.names
129         return False
130
131     def __hash__(self):
132         if self.mbid:
133             return hash(self.mbid)
134         return hash(self.__name)
135
136     def add_alias(self, other):
137         """Add alternative name to `aliases` attibute.
138
139         `other` can be a :class:`sima.lib.meta.Meta` object in which case aliases are merged.
140
141         :param str other: Alias to add, could be any object with ``__str__`` method.
142         """
143         if isinstance(other, Meta):
144             self.__aliases |= other.__aliases
145             self.__aliases -= {self.name}
146         if getattr(other, '__str__', None):
147             if callable(other.__str__) and other.__str__() != self.name:
148                 self.__aliases |= {other.__str__()}
149         else:
150             raise MetaException(f'No __str__ method found in {other!r}')
151
152     @property
153     def name(self):
154         return self.__name
155
156     @property
157     @serialize
158     def name_sz(self):
159         return self.name
160
161     @property
162     def mbid(self):
163         return self.__mbid
164
165     @property
166     def aliases(self):
167         return self.__aliases
168
169     @property
170     @serialize
171     def aliases_sz(self):
172         return self.aliases
173
174     @property
175     def names(self):
176         """aliases + name"""
177         return self.__aliases | {self.__name, }
178
179     @property
180     @serialize
181     def names_sz(self):
182         return self.names
183
184
185 class Album(Meta):
186     """Album object"""
187
188     @mbidfilter
189     def __init__(self, name=None, mbid=None, **kwargs):
190         if kwargs.get('musicbrainz_albumid', False):
191             mbid = kwargs.get('musicbrainz_albumid')
192         super().__init__(name=name, mbid=mbid, **kwargs)
193
194     @property
195     def album(self):
196         return self.name
197
198
199 class Artist(Meta):
200     """Artist object deriving from :class:`Meta`.
201
202     :param str name: Artist name
203     :param str mbid: Musicbrainz artist ID
204     :param str artist: Overrides "name" argument
205     :param str albumartist: use "name" if not set
206     :param str musicbrainz_artistid: Overrides "mbid" argument
207
208     :Example:
209
210     >>> trk = {'artist':'Art Name',
211     >>>        'albumartist': 'Alb Art Name',           # optional
212     >>>        'musicbrainz_artistid': '<UUID4>',       # optional
213     >>>       }
214     >>> artobj0 = Artist(**trk)
215     >>> artobj1 = Artist(name='Tool')
216     """
217
218     @mbidfilter
219     def __init__(self, name=None, mbid=None, **kwargs):
220         if kwargs.get('artist', False):
221             name = kwargs.get('artist')
222         if kwargs.get('musicbrainz_artistid', False):
223             mbid = kwargs.get('musicbrainz_artistid')
224         if name and not kwargs.get('albumartist', False):
225             kwargs['albumartist'] = name.split(SEPARATOR)[0]
226         super().__init__(name=name, mbid=mbid,
227                          albumartist=kwargs.get('albumartist'))
228
229
230 class MetaContainer(Set):
231
232     def __init__(self, iterable):
233         self.elements = lst = []
234         for value in iterable:
235             if value not in lst:
236                 lst.append(value)
237             else:
238                 for inlst in lst:
239                     if value == inlst:
240                         inlst.add_alias(value)
241
242     def __iter__(self):
243         return iter(self.elements)
244
245     def __contains__(self, value):
246         return value in self.elements
247
248     def __len__(self):
249         return len(self.elements)
250
251     def __repr__(self):
252         return repr(self.elements)
253
254 # VIM MODLINE
255 # vim: ai ts=4 sw=4 sts=4 expandtab