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