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