mpd: rewrite connecting/disconnecting functions to use signals.
[nephilim.git] / nephilim / mpclient.py
blobe691989b835080fa963659bc9aea2fd53d6df619
2 # Copyright (C) 2008 jerous <jerous@gmail.com>
3 # Copyright (C) 2009 Anton Khirnov <wyskas@gmail.com>
5 # Nephilim is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # Nephilim is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
19 from PyQt4 import QtCore
20 import mpd
21 import socket
22 import logging
24 from song import Song, SongRef, PlaylistEntryRef
26 class MPClient(QtCore.QObject):
27 """This class offers another layer above pympd, with usefull events."""
28 # public, read-only
29 "a list of audio outputs"
30 outputs = None
32 # private
33 __password = None
34 _client = None
35 _cur_song = None
36 _status = {'volume' : 0, 'repeat' : 0, 'random' : 0,
37 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
38 'time' : 0, 'length' : 0, 'xfade' : 0,
39 'state' : 'stop', 'single' : 0,
40 'consume' : 0}
41 _commands = None
43 _timer_id = None #for querying status changes
44 _db_timer_id = None #for querying db updates
45 _db_update = None #time of last db update
47 logger = None
49 # SIGNALS
50 connect_changed = QtCore.pyqtSignal(bool)
51 db_updated = QtCore.pyqtSignal()
52 song_changed = QtCore.pyqtSignal(object)
53 time_changed = QtCore.pyqtSignal(int)
54 state_changed = QtCore.pyqtSignal(str)
55 volume_changed = QtCore.pyqtSignal(int)
56 repeat_changed = QtCore.pyqtSignal(bool)
57 random_changed = QtCore.pyqtSignal(bool)
58 single_changed = QtCore.pyqtSignal(bool)
59 consume_changed = QtCore.pyqtSignal(bool)
60 playlist_changed = QtCore.pyqtSignal()
62 class Output(QtCore.QObject):
63 """This class represents an MPD audio output."""
64 # public, const
65 mpclient = None
66 name = None
67 id = None
68 state = None
70 # SIGNALS
71 state_changed = QtCore.pyqtSignal(bool)
73 #### public ####
74 def __init__(self, mpclient, name, id, state):
75 QtCore.QObject.__init__(self)
77 self.mpclient = mpclient
78 self.name = name
79 self.id = id
80 self.state = state
82 @QtCore.pyqtSlot(bool)
83 def set_state(self, state):
84 self.mpclient.set_output(self.id, state)
86 #### private ####
87 def mpd_toggle_state(self):
88 """This is called by mpclient to inform about output state change."""
89 self.state = not self.state
90 self.state_changed.emit(self.state)
92 def __init__(self):
93 QtCore.QObject.__init__(self)
95 self.outputs = []
96 self._commands = []
97 self._status = dict(MPClient._status)
98 self.logger = logging.getLogger('mpclient')
100 def connect_mpd(self, host, port, password = None):
101 """Connect to MPD@host:port, optionally using password."""
102 self.logger.info('Connecting to MPD...')
103 if self._client:
104 self.logger.warning('Attempted to connect when already connected.')
105 return
107 self._client = mpd.MPDClient()
108 self._client.connect_changed.connect(lambda val:self.__finish_connect() if val else self.__finish_disconnect())
109 self._client.connect_mpd(host, port)
110 self.__password = password
112 def disconnect_mpd(self):
113 """Disconnect from MPD."""
114 self.logger.info('Disconnecting from MPD...')
115 if not self._client:
116 self.logger.warning('Attempted to disconnect when not connected.')
117 self._client.disconnect_mpd()
119 def password(self, password):
120 """Use the password to authenticate with MPD."""
121 self.logger.info('Authenticating with MPD.')
122 if not self.__check_command_ok('password'):
123 return
124 try:
125 self._client.password(password)
126 self.logger.info('Successfully authenticated')
127 self._commands = list(self._client.commands())
128 except mpd.CommandError:
129 self.logger.error('Incorrect MPD password.')
130 def is_connected(self):
131 """Returns True if connected to MPD, False otherwise."""
132 return self._client != None
134 def status(self):
135 """Get current MPD status."""
136 return self._status
137 def playlistinfo(self):
138 """Returns a list of songs in current playlist."""
139 self.logger.info('Listing current playlist.')
140 if not self.__check_command_ok('playlistinfo'):
141 raise StopIteration
142 for song in self._client.playlistinfo():
143 yield Song(song)
144 raise StopIteration
145 def library(self):
146 """Returns a list of all songs in library."""
147 self.logger.info('Listing library.')
148 if not self.__check_command_ok('listallinfo'):
149 raise StopIteration
150 for song in self._client.listallinfo():
151 if 'file' in song:
152 yield Song(song)
154 raise StopIteration
155 def current_song(self):
156 """Returns the current playing song."""
157 return self._cur_song
158 def is_playing(self):
159 """Returns True if MPD is playing, False otherwise."""
160 return self._status['state'] == 'play'
161 def find(self, *args):
162 if not self.__check_command_ok('find'):
163 raise StopIteration
164 for song in self._client.find(*args):
165 yield Song(song)
166 raise StopIteration
167 def findadd(self, *args):
168 if not self.__check_command_ok('findadd'):
169 return
170 return self._client.findadd(*args)
172 def update_db(self, paths = None):
173 """Starts MPD database update."""
174 self.logger.info('Updating database %s'%(paths if paths else '.'))
175 if not self.__check_command_ok('update'):
176 return
177 if not paths:
178 return self._client.update()
179 self._client.command_list_ok_begin()
180 for path in paths:
181 self._client.update(path)
182 self._client.command_list_end()
185 def volume(self):
186 """Get current volume."""
187 return int(self._status['volume'])
188 def set_volume(self, volume):
189 """Set volume to volume."""
190 self.logger.info('Setting volume to %d.'%volume)
191 if not self.__check_command_ok('setvol'):
192 return
193 volume = min(100, max(0, volume))
194 try:
195 self._client.setvol(volume)
196 except mpd.CommandError, e:
197 self.logger.warning('Error setting volume (probably no outputs enabled): %s.'%e)
199 def urlhandlers(self):
200 """Returns an array of available url handlers."""
201 if not self._client:
202 return []
203 else:
204 return self._client.urlhandlers()
205 def tagtypes(self):
206 """Returns a list of supported tags."""
207 if not self.__check_command_ok('tagtypes'):
208 return []
210 return map(str.lower, list(self._client.tagtypes()) + ['file'])
211 def commands(self):
212 """List all currently available MPD commands."""
213 return self._commands
214 def stats(self):
215 """Get MPD statistics."""
216 return self._client.stats()
218 def repeat(self, val):
219 """Set repeat playlist to val (True/False)."""
220 self.logger.info('Setting repeat to %d.'%val)
221 if not self.__check_command_ok('repeat'):
222 return
223 if isinstance(val, bool):
224 val = 1 if val else 0
225 self._client.repeat(val)
226 def random(self, val):
227 """Set random playback to val (True, False)."""
228 self.logger.info('Setting random to %d.'%val)
229 if not self.__check_command_ok('random'):
230 return
231 if isinstance(val, bool):
232 val = 1 if val else 0
233 self._client.random(val)
234 def crossfade(self, time):
235 """Set crossfading between songs."""
236 self.logger.info('Setting crossfade to %d'%time)
237 if not self.__check_command_ok('crossfade'):
238 return
239 self._client.crossfade(time)
240 def single(self, val):
241 """Set single playback to val (True, False)"""
242 self.logger.info('Setting single to %d.'%val)
243 if not self.__check_command_ok('single'):
244 return
245 if isinstance(val, bool):
246 val = 1 if val else 0
247 self._client.single(val)
248 def consume(self, val):
249 """Set consume mode to val (True, False)"""
250 self.logger.info('Setting consume to %d.'%val)
251 if not self.__check_command_ok('consume'):
252 return
253 if isinstance(val, bool):
254 val = 1 if val else 0
255 self._client.consume(val)
257 def play(self, id = None):
258 """Play song with ID id or next song if id is None."""
259 self.logger.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
260 if not self.__check_command_ok('play'):
261 return
262 if id:
263 self._client.playid(id)
264 else:
265 self._client.playid()
266 def pause(self):
267 """Pause playing."""
268 self.logger.info('Pausing playback.')
269 if not self.__check_command_ok('pause'):
270 return
271 self._client.pause(1)
272 def resume(self):
273 """Resume playing."""
274 self.logger.info('Resuming playback.')
275 if not self.__check_command_ok('pause'):
276 return
277 self._client.pause(0)
278 def next(self):
279 """Move on to the next song in the playlist."""
280 self.logger.info('Skipping to next song.')
281 if not self.__check_command_ok('next'):
282 return
283 self._client.next()
284 def previous(self):
285 """Move back to the previous song in the playlist."""
286 self.logger.info('Moving to previous song.')
287 if not self.__check_command_ok('previous'):
288 return
289 self._client.previous()
290 def stop(self):
291 """Stop playing."""
292 self.logger.info('Stopping playback.')
293 if not self.__check_command_ok('stop'):
294 return
295 self._client.stop()
296 def seek(self, time):
297 """Seek to time (in seconds)."""
298 self.logger.info('Seeking to %d.'%time)
299 if not self.__check_command_ok('seekid'):
300 return
301 if self._status['songid'] > 0:
302 self._client.seekid(self._status['songid'], time)
304 def delete(self, list):
305 """Remove all song IDs in list from the playlist."""
306 if not self.__check_command_ok('deleteid'):
307 return
308 self._client.command_list_ok_begin()
309 try:
310 for id in list:
311 self.logger.info('Deleting id %s from playlist.'%id)
312 self._client.deleteid(id)
313 self._client.command_list_end()
314 except mpd.CommandError, e:
315 self.logger.error('Error deleting files: %s.'%e)
316 def clear(self):
317 """Clear current playlist."""
318 self.logger.info('Clearing playlist.')
319 if not self.__check_command_ok('clear'):
320 return
321 self._client.clear()
322 def add(self, paths):
323 """Add all files in paths to the current playlist."""
324 if not self.__check_command_ok('addid'):
325 return
326 ret = None
327 self._client.command_list_ok_begin()
328 try:
329 for path in paths:
330 self.logger.info('Adding %s to playlist'%path)
331 self._client.addid(path.encode('utf-8'))
332 ret = list(self._client.command_list_end())
333 except mpd.CommandError, e:
334 self.logger.error('Error adding files: %s.'%e)
335 if self._status['state'] == 'stop' and ret:
336 self.play(ret[0])
337 def move(self, source, target):
338 """Move the songs in playlist. Takes a list of source ids and one target position."""
339 self.logger.info('Moving %d to %d.'%(source, target))
340 if not self.__check_command_ok('moveid'):
341 return
342 self._client.command_list_ok_begin()
343 i = 0
344 for id in source:
345 self._client.moveid(id, target + i)
346 i += 1
347 self._client.command_list_end()
349 #### private ####
350 def __finish_connect(self):
351 if self.__password:
352 self.password(self.__password)
353 else:
354 self._commands = list(self._client.commands())
356 if not self.__check_command_ok('listallinfo'):
357 self.logger.error('Don\'t have MPD read permission, diconnecting.')
358 return self.disconnect_mpd()
360 self.__update_current_song()
361 self.__update_outputs()
362 self._db_update = self.stats()['db_update']
364 self.connect_changed.emit(True)
365 self.logger.info('Successfully connected to MPD.')
366 self._timer_id = self.startTimer(500)
367 self._db_timer_id = self.startTimer(1000)
368 def __finish_disconnect(self):
369 self._client = None
371 if self._timer_id:
372 self.killTimer(self._timer_id)
373 self._timer_id = None
374 if self._db_timer_id:
375 self.killTimer(self._db_timer_id)
376 self._db_timer_id = None
377 self._status = dict(MPClient._status)
378 self._cur_song = None
379 self._commands = []
380 self.outputs = []
381 self.connect_changed.emit(False)
382 self.logger.info('Disconnected from MPD.')
383 def __update_current_song(self):
384 """Update the current song."""
385 song = self._client.currentsong()
386 if not song:
387 self._cur_song = None
388 else:
389 self._cur_song = Song(song)
390 def _update_status(self):
391 """Get current status"""
392 if not self._client:
393 return None
394 ret = self._client.status()
395 if not ret:
396 return None
398 ret['repeat'] = int(ret['repeat'])
399 ret['random'] = int(ret['random'])
400 ret['single'] = int(ret['single'])
401 ret['consume'] = int(ret['consume'])
402 if 'time' in ret:
403 cur, len = ret['time'].split(':')
404 ret['length'] = int(len)
405 ret['time'] = int(cur)
406 else:
407 ret['length'] = 0
408 ret['time'] = 0
410 if not 'songid' in ret:
411 ret['songid'] = '-1'
413 return ret
414 def __check_command_ok(self, cmd):
415 if not self._client:
416 return self.logger.info('Not connected.')
417 if not cmd in self._commands:
418 return self.logger.error('Command %s not accessible'%cmd)
419 return True
421 def __update_outputs(self):
422 """Update the list of MPD audio outputs."""
423 if self.__check_command_ok('outputs'):
424 outputs = []
425 for output in self._client.outputs():
426 outputs.append(MPClient.Output(self, output['outputname'], output['outputid'],
427 bool(output['outputenabled'])))
428 self.outputs = outputs
429 else:
430 self.outputs = []
431 def set_output(self, output_id, state):
432 """Set audio output output_id to state (0/1)."""
433 if not self.__check_command_ok('enableoutput'):
434 return
435 if state:
436 self._client.enableoutput(output_id)
437 else:
438 self._client.disableoutput(output_id)
440 def timerEvent(self, event):
441 """Check for changes since last check."""
442 if event.timerId() == self._db_timer_id:
443 #timer for monitoring db changes
444 db_update = self.stats()['db_update']
445 if db_update > self._db_update:
446 self.logger.info('Database updated.')
447 self._db_update = db_update
448 self.db_updated.emit()
449 return
452 old_status = self._status
453 self._status = self._update_status()
455 if not self._status:
456 return self.disconnect_mpd()
458 if self._status['songid'] != old_status['songid']:
459 self.__update_current_song()
460 self.song_changed.emit(PlaylistEntryRef(self, self._status['songid']))
462 if self._status['time'] != old_status['time']:
463 self.time_changed.emit(self._status['time'])
465 if self._status['state'] != old_status['state']:
466 self.state_changed.emit(self._status['state'])
468 if self._status['volume'] != old_status['volume']:
469 self.volume_changed.emit( int(self._status['volume']))
471 if self._status['repeat'] != old_status['repeat']:
472 self.repeat_changed.emit(bool(self._status['repeat']))
474 if self._status['random'] != old_status['random']:
475 self.random_changed.emit(bool(self._status['random']))
477 if self._status['single'] != old_status['single']:
478 self.single_changed.emit(bool(self._status['single']))
480 if self._status['consume'] != old_status['consume']:
481 self.consume_changed.emit(bool(self._status['consume']))
483 if self._status['playlist'] != old_status['playlist']:
484 self.playlist_changed.emit()
486 outputs = list(self._client.outputs())
487 for i in range(len(outputs)):
488 if int(outputs[i]['outputenabled']) != int(self.outputs[i].state):
489 self.outputs[i].mpd_toggle_state()