]> kaliko git repositories - mpd-sima.git/blob - sima/lib/player.py
473c9dfa097dea5f18579c9df0d8dc3d8f03dc89
[mpd-sima.git] / sima / lib / player.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2014 Jack Kaliko <jack@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 # TODO:
22 # Add decorator to filter through history?
23
24 # standard library import
25 import logging
26 from difflib import get_close_matches
27 from itertools import dropwhile
28
29 # local import
30 from .meta import Artist
31 from .simastr import SimaStr
32 from ..utils.leven import levenshtein_ratio
33
34 def blacklist(artist=False, album=False, track=False):
35     #pylint: disable=C0111,W0212
36     field = (album, track)
37     def decorated(func):
38         def wrapper(*args, **kwargs):
39             if not args[0].database:
40                 return func(*args, **kwargs)
41             cls = args[0]
42             boolgen = (bl for bl in field)
43             bl_fun = (cls.database.get_bl_album,
44                       cls.database.get_bl_track,)
45             #bl_getter = next(fn for fn, bl in zip(bl_fun, boolgen) if bl is True)
46             bl_getter = next(dropwhile(lambda _: not next(boolgen), bl_fun))
47             #cls.log.debug('using {0} as bl filter'.format(bl_getter.__name__))
48             results = list()
49             for elem in func(*args, **kwargs):
50                 if bl_getter(elem, add_not=True):
51                     cls.log.debug('Blacklisted "{0}"'.format(elem))
52                     continue
53                 if track and cls.database.get_bl_album(elem, add_not=True):
54                     # filter album as well in track mode
55                     # (artist have already been)
56                     cls.log.debug('Blacklisted alb. "{0.album}"'.format(elem))
57                     continue
58                 results.append(elem)
59             return results
60         return wrapper
61     return decorated
62
63 def bl_artist(func):
64     def wrapper(*args, **kwargs):
65         cls = args[0]
66         if not args[0].database:
67             return func(*args, **kwargs)
68         result = func(*args, **kwargs)
69         if not result:
70             return
71         names = list()
72         for art in result.names:
73             if cls.database.get_bl_artist(art, add_not=True):
74                 cls.log.debug('Blacklisted "{0}"'.format(art))
75                 continue
76             names.append(art)
77         if not names:
78             return
79         resp = Artist(name=names.pop(), mbid=result.mbid)
80         for name in names:
81             resp.add_alias(name)
82         return resp
83     return wrapper
84
85
86 class Player(object):
87     """Player interface to inherit from.
88
89     When querying player music library for tracks, Player instance *must* return
90     Track objects (usually a list of them)
91
92     Player instance should expose the following immutable attributes:
93         * artists
94         * state
95         * current
96         * queue
97         * playlist
98     """
99
100     def __init__(self):
101         super().__init__()
102         self.log = logging.getLogger('sima')
103
104     def monitor(self):
105         """Monitor player for change
106         Returns :
107             * database  player media library has changed
108             * playlist  playlist modified
109             * options   player options changed: repeat mode, etc…
110             * player    player state changed: paused, stopped, skip track…
111         """
112         raise NotImplementedError
113
114     def clean(self):
115         """Any cleanup necessary"""
116         pass
117
118     def remove(self, position=0):
119         """Removes the oldest element of the playlist (index 0)
120         """
121         raise NotImplementedError
122
123     def find_track(self, artist, title=None):
124         """
125         Find tracks for a specific artist or filtering with a track title
126             >>> player.find_track('The Beatles')
127             >>> player.find_track('Nirvana', title='Smells Like Teen Spirit')
128
129         Returns a list of Track objects
130         """
131         raise NotImplementedError
132
133     def find_album(self, artist, album):
134         """
135         Find tracks by track's album name
136             >>> player.find_album('Nirvana', 'Nevermind')
137
138         Returns a list of Track objects
139         """
140         raise NotImplementedError
141
142     def search_albums(self, artist):
143         """
144         Find albums by artist's name
145             >>> art = Artist(name='Nirvana')
146             >>> player.search_albums(art)
147
148         Returns a list of string objects
149         """
150         raise NotImplementedError
151
152     @bl_artist
153     def search_artist(self, artist):
154         """
155         Search artists based on a fuzzy search in the media library
156             >>> bea = player.search_artist('The beatles')
157             >>> print(bea.names)
158             >>> ['The Beatles', 'Beatles', 'the beatles']
159
160         Returns a list of strings (artist names)
161         """
162         found = False
163         # Then proceed with fuzzy matching if got nothing
164         match = get_close_matches(artist.name, self.artists, 50, 0.73)
165         if not match:
166             return
167         if len(match) > 1:
168             self.log.debug('found close match for "%s": %s' %
169                            (artist, '/'.join(match)))
170         # Does not perform fuzzy matching on short and single word strings
171         # Only lowercased comparison
172         if ' ' not in artist.name and len(artist.name) < 8:
173             for fuzz_art in match:
174                 # Regular lowered string comparison
175                 if artist.name.lower() == fuzz_art.lower():
176                     artist.add_alias(fuzz_art)
177                     return artist
178         fzartist = SimaStr(artist.name)
179         for fuzz_art in match:
180             # Regular lowered string comparison
181             if artist.name.lower() == fuzz_art.lower():
182                 found = True
183                 if artist.name != fuzz_art:
184                     artist.add_alias(fuzz_art)
185                     self.log.debug('"%s" matches "%s".' % (fuzz_art, artist))
186                 continue
187             # SimaStr string __eq__ (not regular string comparison here)
188             if fzartist == fuzz_art:
189                 found = True
190                 artist.add_alias(fuzz_art)
191                 self.log.info('"%s" quite probably matches "%s" (SimaStr)' %
192                               (fuzz_art, artist))
193             #else:
194                 #self.log.debug('FZZZ: "%s" does not match "%s"' %
195                                #(fuzz_art, artist))
196         if found:
197             if artist.aliases:
198                 self.log.debug('Found: {}'.format('/'.join(artist.names)))
199             return artist
200         return
201
202     def disconnect(self):
203         """Closing client connection with the Player
204         """
205         raise NotImplementedError
206
207     def connect(self):
208         """Connect client to the Player
209         """
210         raise NotImplementedError
211
212     @property
213     def artists(self):
214         raise NotImplementedError
215
216     @property
217     def state(self):
218         raise NotImplementedError
219
220     @property
221     def current(self):
222         raise NotImplementedError
223
224     @property
225     def queue(self):
226         raise NotImplementedError
227
228     @property
229     def playlist(self):
230         raise NotImplementedError
231
232 # VIM MODLINE
233 # vim: ai ts=4 sw=4 sts=4 expandtab