4 This client is built above python-musicpd a fork of python-mpd
6 # pylint: disable=C0111
8 # standart library import
10 from difflib import get_close_matches
11 from select import select
13 # third parties componants
15 from musicpd import (MPDClient, MPDError, CommandError)
16 except ImportError as err:
17 from sys import exit as sexit
18 print('ERROR: missing python-musicpd?\n{0}'.format(err))
22 from .lib.player import Player
23 from .lib.track import Track
24 from .lib.simastr import SimaStr
27 class PlayerError(Exception):
28 """Fatal error in poller."""
30 class PlayerCommandError(PlayerError):
33 PlayerUnHandledError = MPDError # pylint: disable=C0103
35 class PlayerClient(Player):
39 _fetch_item single str
40 _fetch_object single dict
41 _fetch_list list of str
42 _fetch_playlist list of str
43 _fetch_changes list of dict
44 _fetch_database list of dict
45 _fetch_songs list of dict, especially tracks
47 TODO: handle exception in command not going through _client_wrapper() (ie.
50 def __init__(self, host="localhost", port="6600", password=None):
52 self._comm = self._args = None
53 self._mpd = host, port, password
54 self._client = MPDClient()
55 self._client.iterate = True
57 def __getattr__(self, attr):
59 wrapper = self._execute
60 return lambda *args: wrapper(command, args)
62 def _execute(self, command, args):
63 self._write_command(command, args)
64 return self._client_wrapper()
66 def _write_command(self, command, args=[]):
70 self._args.append(arg)
72 def _client_wrapper(self):
73 func = self._client.__getattr__(self._comm)
75 ans = func(*self._args)
76 # WARNING: MPDError is an ancestor class of # CommandError
77 except CommandError as err:
78 raise PlayerCommandError('MPD command error: %s' % err)
79 except (MPDError, IOError) as err:
80 raise PlayerError(err)
81 return self._track_format(ans)
83 def _track_format(self, ans):
85 unicode_obj = ["idle", "listplaylist", "list", "sticker list",
86 "commands", "notcommands", "tagtypes", "urlhandlers",]
88 # TODO: ain't working for "sticker find" and "sticker list"
89 tracks_listing = ["playlistfind", "playlistid", "playlistinfo",
90 "playlistsearch", "plchanges", "listplaylistinfo", "find",
91 "search", "sticker find",]
92 track_obj = ['currentsong']
93 if self._comm in tracks_listing + track_obj:
94 # pylint: disable=w0142
95 if isinstance(ans, list):
96 return [Track(**track) for track in ans]
97 elif isinstance(ans, dict):
101 def __skipped_track(self, old_curr):
102 if (self.state == 'stop'
103 or not hasattr(old_curr, 'id')
104 or not hasattr(self.current, 'id')):
106 return (self.current.id != old_curr.id) # pylint: disable=no-member
108 def find_track(self, artist, title=None):
109 #return getattr(self, 'find')('artist', artist, 'title', title)
111 return self.find('artist', artist, 'title', title)
112 return self.find('artist', artist)
114 def fuzzy_find(self, art):
116 Controls presence of artist in music library.
117 Crosschecking artist names with SimaStr objects / difflib / levenshtein
119 TODO: proceed crosschecking even when an artist matched !!!
120 Not because we found "The Doors" as "The Doors" that there is no
121 remaining entries as "Doors" :/
122 not straight forward, need probably heavy refactoring.
124 matching_artists = list()
125 artist = SimaStr(art)
126 all_artists = self.list('artist')
128 # Check against the actual string in artist list
129 if artist.orig in all_artists:
130 self.log.debug('found exact match for "%s"' % artist)
132 # Then proceed with fuzzy matching if got nothing
133 match = get_close_matches(artist.orig, all_artists, 50, 0.73)
136 self.log.debug('found close match for "%s": %s' %
137 (artist, '/'.join(match)))
138 # Does not perform fuzzy matching on short and single word strings
139 # Only lowercased comparison
140 if ' ' not in artist.orig and len(artist) < 8:
141 for fuzz_art in match:
142 # Regular string comparison SimaStr().lower is regular string
143 if artist.lower() == fuzz_art.lower():
144 matching_artists.append(fuzz_art)
145 self.log.debug('"%s" matches "%s".' % (fuzz_art, artist))
146 return matching_artists
147 for fuzz_art in match:
148 # Regular string comparison SimaStr().lower is regular string
149 if artist.lower() == fuzz_art.lower():
150 matching_artists.append(fuzz_art)
151 self.log.debug('"%s" matches "%s".' % (fuzz_art, artist))
152 return matching_artists
153 # SimaStr string __eq__ (not regular string comparison here)
154 if artist == fuzz_art:
155 matching_artists.append(fuzz_art)
156 self.log.info('"%s" quite probably matches "%s" (SimaStr)' %
159 self.log.debug('FZZZ: "%s" does not match "%s"' %
161 return matching_artists
163 def find_album(self, artist, album):
165 Special wrapper around album search:
166 Album lookup is made through AlbumArtist/Album instead of Artist/Album
168 alb_art_search = self.find('albumartist', artist, 'album', album)
170 return alb_art_search
171 return self.find('artist', artist, 'album', album)
173 def find_albums(self, artist):
175 Special wrapper around album search:
176 Album lookup is made through AlbumArtist/Album instead of Artist/Album
178 alb_art_search = self.list('album', 'albumartist', artist,)
180 return alb_art_search
181 return self.list('album', 'artist', artist)
186 self._client.send_idle('database', 'playlist', 'player', 'options')
187 select([self._client], [], [], 60)
188 ret = self._client.fetch_idle()
189 if self.__skipped_track(curr):
190 ret.append('skipped')
192 except (MPDError, IOError) as err:
193 raise PlayerError("Couldn't init idle: %s" % err)
195 def remove(self, position=0):
196 self._client.delete(position)
198 def add(self, track):
199 """Overriding MPD's add method to accept add signature with a Track
201 self._client.add(track.file)
205 return str(self._client.status().get('state'))
209 return self.currentsong()
215 return [ trk for trk in plst if int(trk.pos) > int(self.current.pos)]
220 Override deprecated MPD playlist command
222 return self.playlistinfo()
225 host, port, password = self._mpd
228 self._client.connect(host, port)
230 # Catch socket errors
231 except IOError as err:
232 raise PlayerError('Could not connect to "%s:%s": %s' %
233 (host, port, err.strerror))
235 # Catch all other possible errors
236 # ConnectionError and ProtocolError are always fatal. Others may not
237 # be, but we don't know how to handle them here, so treat them as if
238 # they are instead of ignoring them.
239 except MPDError as err:
240 raise PlayerError('Could not connect to "%s:%s": %s' %
245 self._client.password(password)
247 # Catch errors with the password command (e.g., wrong password)
248 except CommandError as err:
249 raise PlayerError("Could not connect to '%s': "
250 "password command failed: %s" %
253 # Catch all other possible errors
254 except (MPDError, IOError) as err:
255 raise PlayerError("Could not connect to '%s': "
256 "error with password command: %s" %
258 # Controls we have sufficient rights
259 needed_cmds = ['status', 'stats', 'add', 'find', \
260 'search', 'currentsong', 'ping']
262 available_cmd = self._client.commands()
263 for nddcmd in needed_cmds:
264 if nddcmd not in available_cmd:
266 raise PlayerError('Could connect to "%s", '
267 'but command "%s" not available' %
270 def disconnect(self):
271 # Try to tell MPD we're closing the connection first
274 # If that fails, don't worry, just ignore it and disconnect
275 except (MPDError, IOError):
278 self._client.disconnect()
279 # Disconnecting failed, so use a new client object instead
280 # This should never happen. If it does, something is seriously broken,
281 # and the client object shouldn't be trusted to be re-used.
282 except (MPDError, IOError):
283 self._client = MPDClient()
286 # vim: ai ts=4 sw=4 sts=4 expandtab