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