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