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