mpclient: add a new asynchronous high-level MPD layer
[nephilim.git] / nephilim / mpclient.py
blobc118cdbcde83009516b16de5d44b4efcf96f7d54
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, QtNetwork
20 from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
21 import mpd
22 import socket
23 import logging
25 from song import Song, PlaylistEntryRef
26 from mpdsocket import MPDSocket
28 class MPClient(QtCore.QObject):
29 """This class offers another layer above pympd, with usefull events."""
30 # public, read-only
31 logger = None
33 # these don't change while mpd is running
34 outputs = None
35 tagtypes = None
36 urlhandlers = None
37 commands = None
39 # private
40 __password = None
41 _client = None
42 _cur_song = None
43 _status = {'volume' : 0, 'repeat' : 0, 'random' : 0,
44 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
45 'time' : 0, 'length' : 0, 'xfade' : 0,
46 'state' : 'stop', 'single' : 0,
47 'consume' : 0}
49 _timer_id = None #for querying status changes
50 _db_timer_id = None #for querying db updates
51 _db_update = None #time of last db update
53 __stats = {'artists': '0', 'albums' : '0', 'songs' : '0', 'uptime' : '0',
54 'playtime' : '0', 'db_playtime' : '0', 'db_update' : '0'}
56 # SIGNALS
57 connect_changed = QtCore.pyqtSignal(bool)
58 db_updated = QtCore.pyqtSignal()
59 song_changed = QtCore.pyqtSignal(object)
60 time_changed = QtCore.pyqtSignal(int)
61 state_changed = QtCore.pyqtSignal(str)
62 volume_changed = QtCore.pyqtSignal(int)
63 repeat_changed = QtCore.pyqtSignal(bool)
64 random_changed = QtCore.pyqtSignal(bool)
65 single_changed = QtCore.pyqtSignal(bool)
66 consume_changed = QtCore.pyqtSignal(bool)
67 playlist_changed = QtCore.pyqtSignal()
70 def __init__(self):
71 QtCore.QObject.__init__(self)
72 self.logger = logging.getLogger('mpclient')
73 self.__update_static()
74 self._status = dict(MPClient._status)
76 def connect_mpd(self, host, port, password = None):
77 """Connect to MPD@host:port, optionally using password."""
78 self.logger.info('Connecting to MPD...')
79 if self._client:
80 self.logger.warning('Attempted to connect when already connected.')
81 return
83 self._client = mpd.MPDClient()
84 self._client.connect_changed.connect(lambda val:self.__finish_connect() if val else self.__finish_disconnect())
85 self._client.connect_mpd(host, port)
86 self.__password = password
88 def disconnect_mpd(self):
89 """Disconnect from MPD."""
90 self.logger.info('Disconnecting from MPD...')
91 if self._client:
92 self._client.disconnect_mpd()
94 def password(self, password):
95 """Use the password to authenticate with MPD."""
96 self.logger.info('Authenticating with MPD.')
97 if not self.__check_command_ok('password'):
98 return
99 try:
100 self._client.password(password)
101 self.logger.info('Successfully authenticated')
102 self.__update_static()
103 except mpd.CommandError:
104 self.logger.error('Incorrect MPD password.')
105 def is_connected(self):
106 """Returns True if connected to MPD, False otherwise."""
107 return self._client != None
109 def status(self):
110 """Get current MPD status."""
111 return self._status
112 def playlistinfo(self):
113 """Returns a list of songs in current playlist."""
114 self.logger.info('Listing current playlist.')
115 if not self.__check_command_ok('playlistinfo'):
116 raise StopIteration
117 for song in self._client.playlistinfo():
118 yield Song(song)
119 raise StopIteration
120 def library(self):
121 """Returns a list of all songs in library."""
122 self.logger.info('Listing library.')
123 if not self.__check_command_ok('listallinfo'):
124 raise StopIteration
125 for song in self._client.listallinfo():
126 if 'file' in song:
127 yield Song(song)
129 raise StopIteration
130 def current_song(self):
131 """Returns the current playing song."""
132 return self._cur_song
133 def is_playing(self):
134 """Returns True if MPD is playing, False otherwise."""
135 return self._status['state'] == 'play'
136 def find(self, *args):
137 if not self.__check_command_ok('find'):
138 raise StopIteration
139 for song in self._client.find(*args):
140 yield Song(song)
141 raise StopIteration
142 def findadd(self, *args):
143 """Find tracks with given tags and add them to playlist. Takes
144 a list of (tag, value)."""
145 self.logger.info('Findadd %s.'%unicode(args))
146 if not self.__check_command_ok('findadd'):
147 return
148 return self._client.findadd(*args)
149 def playlistid(self, plid):
150 """Return a song with a given playlist id."""
151 self.logger.info('Getting id %s.'%('of id %s'%(plid) if plid else ''))
152 if not self.__check_command_ok('play'):
153 return
154 ret = None
155 for it in self._client.playlistid(plid):
156 ret = Song(it)
157 return ret
159 def update_db(self, paths = None):
160 """Starts MPD database update."""
161 self.logger.info('Updating database %s'%(paths if paths else '.'))
162 if not self.__check_command_ok('update'):
163 return
164 if not paths:
165 return self._client.update()
166 self._client.command_list_ok_begin()
167 for path in paths:
168 self._client.update(path)
169 list(self._client.command_list_end())
171 def volume(self):
172 """Get current volume."""
173 return int(self._status['volume'])
174 def set_volume(self, volume):
175 """Set volume to volume."""
176 self.logger.info('Setting volume to %d.'%volume)
177 if not self.__check_command_ok('setvol'):
178 return
179 volume = min(100, max(0, volume))
180 try:
181 self._client.setvol(volume)
182 except mpd.CommandError, e:
183 self.logger.warning('Error setting volume (probably no outputs enabled): %s.'%e)
185 def stats(self):
186 """Get MPD statistics."""
187 if not self.__check_command_ok('stats'):
188 return self.__stats
189 return self._client.stats()
191 def repeat(self, val):
192 """Set repeat playlist to val (True/False)."""
193 self.logger.info('Setting repeat to %d.'%val)
194 if not self.__check_command_ok('repeat'):
195 return
196 if isinstance(val, bool):
197 val = 1 if val else 0
198 self._client.repeat(val)
199 def random(self, val):
200 """Set random playback to val (True, False)."""
201 self.logger.info('Setting random to %d.'%val)
202 if not self.__check_command_ok('random'):
203 return
204 if isinstance(val, bool):
205 val = 1 if val else 0
206 self._client.random(val)
207 def crossfade(self, time):
208 """Set crossfading between songs."""
209 self.logger.info('Setting crossfade to %d'%time)
210 if not self.__check_command_ok('crossfade'):
211 return
212 self._client.crossfade(time)
213 def single(self, val):
214 """Set single playback to val (True, False)"""
215 self.logger.info('Setting single to %d.'%val)
216 if not self.__check_command_ok('single'):
217 return
218 if isinstance(val, bool):
219 val = 1 if val else 0
220 self._client.single(val)
221 def consume(self, val):
222 """Set consume mode to val (True, False)"""
223 self.logger.info('Setting consume to %d.'%val)
224 if not self.__check_command_ok('consume'):
225 return
226 if isinstance(val, bool):
227 val = 1 if val else 0
228 self._client.consume(val)
230 def play(self, id = None):
231 """Play song with ID id or next song if id is None."""
232 self.logger.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
233 if not self.__check_command_ok('play'):
234 return
235 if id:
236 self._client.playid(id)
237 else:
238 self._client.playid()
239 def pause(self):
240 """Pause playing."""
241 self.logger.info('Pausing playback.')
242 if not self.__check_command_ok('pause'):
243 return
244 self._client.pause(1)
245 def resume(self):
246 """Resume playing."""
247 self.logger.info('Resuming playback.')
248 if not self.__check_command_ok('pause'):
249 return
250 self._client.pause(0)
251 def next(self):
252 """Move on to the next song in the playlist."""
253 self.logger.info('Skipping to next song.')
254 if not self.__check_command_ok('next'):
255 return
256 self._client.next()
257 def previous(self):
258 """Move back to the previous song in the playlist."""
259 self.logger.info('Moving to previous song.')
260 if not self.__check_command_ok('previous'):
261 return
262 self._client.previous()
263 def stop(self):
264 """Stop playing."""
265 self.logger.info('Stopping playback.')
266 if not self.__check_command_ok('stop'):
267 return
268 self._client.stop()
269 def seek(self, time):
270 """Seek to time (in seconds)."""
271 self.logger.info('Seeking to %d.'%time)
272 if not self.__check_command_ok('seekid'):
273 return
274 if self._status['songid'] > 0:
275 self._client.seekid(self._status['songid'], time)
277 def delete(self, ids):
278 """Remove all song IDs in list from the playlist."""
279 if not self.__check_command_ok('deleteid'):
280 return
281 self._client.command_list_ok_begin()
282 try:
283 for id in ids:
284 self.logger.info('Deleting id %s from playlist.'%id)
285 self._client.deleteid(id)
286 list(self._client.command_list_end())
287 except mpd.CommandError, e:
288 self.logger.error('Error deleting files: %s.'%e)
289 def clear(self):
290 """Clear current playlist."""
291 self.logger.info('Clearing playlist.')
292 if not self.__check_command_ok('clear'):
293 return
294 self._client.clear()
295 def add(self, paths, pos = -1):
296 """Add all files in paths to the current playlist."""
297 if not self.__check_command_ok('addid'):
298 return
299 ret = None
300 self._client.command_list_ok_begin()
301 for path in paths:
302 self.logger.info('Adding %s to playlist'%path)
303 if pos < 0:
304 self._client.addid(path)
305 else:
306 self._client.addid(path, pos)
307 pos += 1
308 try:
309 ret = list(self._client.command_list_end())
310 except mpd.CommandError, e:
311 self.logger.error('Error adding files: %s.'%e)
312 if self._status['state'] == 'stop' and ret:
313 self.play(ret[0])
314 def move(self, source, target):
315 """Move the songs in playlist. Takes one source id and one target position."""
316 self.logger.info('Moving %s to %s.'%(source, target))
317 if not self.__check_command_ok('moveid'):
318 return
319 self._client.moveid(source, target)
321 #### private ####
322 def __finish_connect(self):
323 if self.__password:
324 self.password(self.__password)
325 else:
326 self.__update_static()
328 if not self.__check_command_ok('listallinfo'):
329 self.logger.error('Don\'t have MPD read permission, diconnecting.')
330 return self.disconnect_mpd()
332 self.__update_current_song()
333 self._db_update = self.stats()['db_update']
335 self.connect_changed.emit(True)
336 self.logger.info('Successfully connected to MPD.')
337 self._timer_id = self.startTimer(500)
338 self._db_timer_id = self.startTimer(1000)
339 def __finish_disconnect(self):
340 self._client = None
342 if self._timer_id:
343 self.killTimer(self._timer_id)
344 self._timer_id = None
345 if self._db_timer_id:
346 self.killTimer(self._db_timer_id)
347 self._db_timer_id = None
348 self._status = dict(MPClient._status)
349 self._cur_song = None
350 self.__update_static()
351 self.connect_changed.emit(False)
352 self.logger.info('Disconnected from MPD.')
353 def __update_current_song(self):
354 """Update the current song."""
355 song = self._client.currentsong()
356 if not song:
357 self._cur_song = None
358 else:
359 self._cur_song = Song(song)
360 def _update_status(self):
361 """Get current status"""
362 if not self._client:
363 return None
364 ret = self._client.status()
365 if not ret:
366 return None
368 ret['repeat'] = int(ret['repeat'])
369 ret['random'] = int(ret['random'])
370 ret['single'] = int(ret['single'])
371 ret['consume'] = int(ret['consume'])
372 ret['volume'] = int(ret['volume'])
373 if 'time' in ret:
374 cur, len = ret['time'].split(':')
375 ret['length'] = int(len)
376 ret['time'] = int(cur)
377 else:
378 ret['length'] = 0
379 ret['time'] = 0
381 if not 'songid' in ret:
382 ret['songid'] = '-1'
384 return ret
385 def __check_command_ok(self, cmd):
386 if not self._client:
387 return self.logger.info('Not connected.')
388 if not cmd in self.commands:
389 return self.logger.error('Command %s not accessible'%cmd)
390 return True
392 def __update_static(self):
393 """Update static values, called on connect/disconnect."""
394 if self._client:
395 self.commands = list(self._client.commands())
396 else:
397 self.commands = []
399 if self.__check_command_ok('outputs'):
400 outputs = []
401 for output in self._client.outputs():
402 outputs.append(AudioOutput(self, output['outputname'], output['outputid'],
403 bool(output['outputenabled'])))
404 self.outputs = outputs
405 else:
406 self.outputs = []
408 if self.__check_command_ok('tagtypes'):
409 self.tagtypes = map(unicode.lower, self._client.tagtypes()) + ['file']
410 else:
411 self.tagtypes = []
413 if self.__check_command_ok('urlhandlers'):
414 self.urlhandlers = list(self._client.urlhandlers())
415 else:
416 self.urlhandlers = []
418 def set_output(self, output_id, state):
419 """Set audio output output_id to state (0/1). Called only by AudioOutput."""
420 if not self.__check_command_ok('enableoutput'):
421 return
422 if state:
423 self._client.enableoutput(output_id)
424 else:
425 self._client.disableoutput(output_id)
427 def timerEvent(self, event):
428 """Check for changes since last check."""
429 if event.timerId() == self._db_timer_id:
430 #timer for monitoring db changes
431 db_update = self.stats()['db_update']
432 if db_update > self._db_update:
433 self.logger.info('Database updated.')
434 self._db_update = db_update
435 self.db_updated.emit()
436 return
439 old_status = self._status
440 self._status = self._update_status()
442 if not self._status:
443 self.logger.error('Error reading status.')
444 return self.disconnect_mpd()
446 if self._status['songid'] != old_status['songid']:
447 self.__update_current_song()
448 self.song_changed.emit(PlaylistEntryRef(self, self._status['songid']))
450 if self._status['time'] != old_status['time']:
451 self.time_changed.emit(self._status['time'])
453 if self._status['state'] != old_status['state']:
454 self.state_changed.emit(self._status['state'])
456 if self._status['volume'] != old_status['volume']:
457 self.volume_changed.emit( int(self._status['volume']))
459 if self._status['repeat'] != old_status['repeat']:
460 self.repeat_changed.emit(bool(self._status['repeat']))
462 if self._status['random'] != old_status['random']:
463 self.random_changed.emit(bool(self._status['random']))
465 if self._status['single'] != old_status['single']:
466 self.single_changed.emit(bool(self._status['single']))
468 if self._status['consume'] != old_status['consume']:
469 self.consume_changed.emit(bool(self._status['consume']))
471 if self._status['playlist'] != old_status['playlist']:
472 self.playlist_changed.emit()
474 outputs = list(self._client.outputs())
475 for i in range(len(outputs)):
476 if int(outputs[i]['outputenabled']) != int(self.outputs[i].state):
477 self.outputs[i].mpd_toggle_state()
480 class AudioOutput(QtCore.QObject):
481 """This class represents an MPD audio output."""
482 # public, const
483 mpclient = None
484 name = None
485 id = None
486 state = None
488 # SIGNALS
489 state_changed = QtCore.pyqtSignal(bool)
491 #### public ####
492 def __init__(self, mpclient, name, id, state):
493 QtCore.QObject.__init__(self)
495 self.mpclient = mpclient
496 self.name = name
497 self.id = id
498 self.state = state
500 @QtCore.pyqtSlot(bool)
501 def set_state(self, state):
502 self.mpclient.set_output(self.id, state)
504 #### private ####
505 def mpd_toggle_state(self):
506 """This is called by mpclient to inform about output state change."""
507 self.state = not self.state
508 self.state_changed.emit(self.state)
510 class AudioOutput2(QtCore.QObject):
511 """This class represents an MPD audio output."""
513 #### PUBLIC ####
514 # constants
515 name = None
517 # read-only
518 state = None
520 # SIGNALS
521 state_changed = QtCore.pyqtSignal(bool)
523 #### public ####
524 def __init__(self, data, set_state, parent = None):
525 QtCore.QObject.__init__(self, parent)
527 self.name = data['outputname']
528 self.state = int(data['outputenabled'])
530 self.set_state = set_state
532 @Slot(bool)
533 def set_state(self, state):
534 pass
536 def update(self, data):
538 This is called by mpclient to inform about output state change.
540 if int(data['outputenabled']) != self.state:
541 self.state = not self.state
542 self.state_changed.emit(self.state)
544 class MPDStatus(dict):
545 _status = {'volume' : 0, 'repeat' : 0, 'single' : 0,
546 'consume' : 0, 'playlist' : '-1', 'playlistlength' : 0,
547 'state' : 'stop', 'song' : -1, 'songid' : '-1',
548 'nextsong' : -1, 'nextsongid' : '-1', 'time' : '0:0',
549 'elapsed' : .0, 'bitrate' : 0, 'xfade' : 0,
550 'mixrampdb' : .0, 'mixrampdelay' : .0, 'audio' : '0:0:0',
551 'updatings_db' : -1, 'error' : '', 'random' : 0 }
553 def __init__(self, data = {}):
554 dict.__init__(self, MPDStatus._status)
555 for key in data:
556 if key in self._status:
557 self[key] = type(self._status[key])(data[key])
558 else:
559 self[key] = data[key]
560 try:
561 self['time'] = map(int, self['time'].split(':'))
562 except ValueError:
563 self['time'] = [0, 0]
564 try:
565 self['audio'] = tuple(map(int, self['audio'].split(':')))
566 except ValueError:
567 self['audio'] = (0, 0, 0)
569 class MPClient2(QtCore.QObject):
571 A high-level MPD interface. It is mostly asynchronous -- all responses from
572 MPD are read via callbacks. A callback may be None, in which case the data
573 is silently discarded. Callbacks that take iterators must ensure that the
574 iterator is exhausted.
577 #### PUBLIC ####
578 # these don't change while we are connected
579 """A list of AudioOutputs available."""
580 outputs = None
581 """A list of supported tags (valid indices for Song)."""
582 tagtypes = None
583 """A list of supported URL handlers."""
584 urlhandlers = None
586 # read-only
587 """An MPDStatus object representing current status."""
588 status = None
589 """A Song object representing current song."""
590 cur_song = None
592 # SIGNALS
593 connect_changed = Signal(bool)
594 db_updated = Signal()
595 time_changed = Signal(int)
596 song_changed = Signal(object)
597 state_changed = Signal(str)
598 volume_changed = Signal(int)
599 repeat_changed = Signal(bool)
600 random_changed = Signal(bool)
601 single_changed = Signal(bool)
602 consume_changed = Signal(bool)
603 playlist_changed = Signal()
606 #### PRIVATE ####
607 # const
608 _sup_ver = (0, 16, 0)
609 _logger = None
610 _timer = None
612 # these don't change while we are connected
613 _commands = None
614 _socket = None
615 _password = None
617 #### PUBLIC ####
618 def __init__(self, parent = None):
619 QtCore.QObject.__init__(self, parent)
620 self._logger = logging.getLogger('%smpclient'%(unicode(parent) + "." if parent else ""))
621 self._timer = QtCore.QTimer(self)
622 self._timer.setInterval(1000)
623 self._timer.timeout.connect(self._update_timer)
624 self._socket = MPDSocket(self)
625 self._commands = []
626 self.status = MPDStatus()
627 self.cur_song = Song()
629 self.outputs = []
630 self.urlhandlers = []
631 self.tagtypes = []
632 def __str__(self):
633 return self._logger.name
635 def connect_mpd(self, host = "localhost", port = 6600, password = None):
637 Connect to MPD at host:port optionally using a password. A Unix domain
638 socket is used if port is omitted.
640 self._logger.info('Connecting to MPD...')
642 if self.is_connected():
643 self._logger.warning('Already connected.')
645 self._socket.connect_changed.connect(lambda val: self._handle_connected() if val else self._handle_disconnected())
646 self._socket.connect_mpd(host, port)
647 self._password = password
649 def disconnect_mpd(self):
651 Disconnect from MPD.
653 self._logger.info('Disconnecting from MPD.')
654 if self.is_connected():
655 self._socket.write_command('close')
657 def is_connected(self):
659 Returns True if connected to MPD, False otherwise.
661 return self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState
663 def playlist(self, callback):
665 Request current playlist from MPD. Callback will be called with an
666 iterator over Songs in current playlist as the argument.
668 self._command('playlistinfo', callback = lambda data: callback(self._parse_songs(data)))
669 def database(self, callback):
671 Request database information from MPD. Callback will be called with an
672 iterator over all Songs in the database as the argument.
674 self._command('listallinfo', callback = lambda data: callback(self._parse_songs(data)))
676 def find(self, callback, *args):
678 Request a search on MPD. Callback will be called with an iterator over
679 all found songs. For allowed values of args, see MPD protocol documentation.
681 self._command('find', args, callback = lambda data: callback(self._parse_songs(data)))
682 def find_sync(self, *args):
684 Search for songs on MPD synchronously. Returns an iterator over all
685 found songs. For allowed values of args, see MPD protocol documentation.
687 return self._command_sync('find', args, parse = lambda data: self._parse_songs(data))
688 def findadd(self, *args):
690 Request a search on MPD and add found songs to current playlist. Allowed values
691 of args are same as for find.
693 self._command('findadd', *args)
695 def get_plist_song(self, plid):
697 Get a song with a given playlist id synchronously.
699 return self._command_sync('playlistid', plid, parse = lambda data: Song(list(self._parse_objects(data, []))[0]))
700 def set_volume(self, volume):
702 Set MPD volume level.
704 volume = min(100, max(0, volume))
705 self._command('setvol', volume)
706 def repeat(self, val):
708 Enable/disable repeat.
710 val = '1' if val else '0'
711 self._command('repeat', val)
712 def random(self, val):
714 Enable/disable random.
716 val = '1' if val else '0'
717 self._command('random', val)
718 def consume(self, val):
720 Enable/disable consume.
722 val = '1' if val else '0'
723 self._command('consume', val)
724 def single(self, val):
726 Enable/disable single.
728 val = '1' if val else '0'
729 self._command('single', val)
730 def crossfade(self, time):
732 Set crossfade to specified time.
734 self._command('crossfade', val)
736 def play(self, id = None):
738 Start playback of song with a specified id. If no id is given, then
739 start on current song/beginning.
741 args = ['playid']
742 if id:
743 args.append(id)
744 self._command(*args)
745 def pause(self):
747 Pause playback.
749 self._command('pause', 1)
750 def resume(self):
752 Resume paused playback.
754 self._command('pause', 0)
755 def next(self):
757 Move on to next song.
759 self._command('next')
760 def previous(self):
762 Move back to previous song.
764 self._command('previous')
765 def stop(self):
767 Stop playback.
769 self._command('stop')
770 def seek(self, time):
772 Seek to specified time in current song.
774 self._command('seekid', self.status['songid'], time)
776 def delete(self, ids):
778 Delete songs with specified ids from playlist.
780 for id in ids:
781 self._command('deleteid', id)
782 def clear(self):
784 Clear current playlist.
786 self._command('clear')
787 def add(self, paths, pos = -1):
789 Add specified songs to specified position in current playlist.
791 # start playback of the first added song if MPD is stopped
792 if self.status['state'] == 'stop':
793 cb = lambda data: [self.play(sid) for sid in self._parse_list(data) ]
794 else:
795 cb = None
797 args = ['addid', '']
798 if pos >= 0:
799 args.append(pos)
800 for path in paths:
801 args[1] = path
802 if cb:
803 self._command(*args, callback = cb)
804 cb = None
805 else:
806 self._command(*args)
807 if pos >= 0:
808 args[2] += 1
809 def move(self, src, dst):
811 Move a song with given src id to position dst.
813 self._command('moveid', src, dst)
815 #### PRIVATE ####
817 ## connection functions ##
818 # these functions are called during connection process #
819 # XXX: maybe use a generator?
820 @Slot()
821 def _handle_connected(self):
823 Called when a connection is established. Send a password and
824 start getting locally stored values.
826 self._logger.debug('Connection established.')
828 # check if protocol version is supported
829 v = self._socket.version
830 if v[0] != self._sup_ver[0]:
831 self._logger.error('Server reported unsupported major protocol version %d, disconnecting.'%v[0])
832 return self.disconnect_mpd()
833 if v[1] < self._sup_ver[1]:
834 self._logger.warning('Server reported too low minor protocol version %d. Continuing, but things might break.'%v[1])
836 if self._password:
837 self._socket.write_command('password', self._password)
839 self._socket.write_command('commands', callback = self._parse_commands)
841 def _parse_commands(self, data):
843 Receive a list of available commands and update
844 the other locally stored values.
846 self._logger.debug('Receiving command list.')
847 self._commands = list(self._parse_list(data))
849 if not 'listallinfo' in self._commands:
850 self._logger.error('Don\'t have MPD read permission, diconnecting.')
851 return self.disconnect_mpd()
853 # update cached values
854 self._command('outputs', callback = self._parse_outputs)
855 self._command('tagtypes', callback = self._parse_tagtypes)
856 self._command('urlhandlers', callback = self._parse_urlhandlers)
858 def _parse_outputs(self, data):
860 Update a list of outputs.
862 self._logger.debug('Receiving outputs.')
863 self.outputs = []
864 for output in self._parse_objects(data, ['outputid']):
865 self.outputs.append(AudioOutput(output, lambda val, outid = output['outputid']: self._set_output(outid, val), self))
867 def _parse_tagtypes(self, data):
869 Update a list of tag types.
871 self._logger.debug('Receiving tag types.')
872 self.tagtypes = list(self._parse_list(data)) + ['file']
873 def _parse_urlhandlers(self, data):
875 Update a list of URL handlers and finish connection.
877 self._logger.debug('Receiving URL handlers.')
878 self.urlhandlers = list(self._parse_list(data))
880 # done initializing data, finish connecting
881 return self._finish_connect()
883 def _finish_connect(self):
885 Called when connecting is completely done. Emit all signals.
887 self._logger.info('Successfully connected to MPD.')
889 self._socket.subsystems_changed.connect(self._mpd_changed)
890 self.connect_changed.emit(True)
891 self._mpd_changed()
893 @Slot()
894 def _handle_disconnected(self):
896 Called when connection is closed. Clear all cached data and emit
897 corresponding signals.
899 self._logger.info('Disconnected from MPD.')
900 self._commands = []
901 self.outputs = {}
902 self.tagtypes = []
903 self.urlhandlers = []
905 self._mpd_changed()
906 self.connect_changed.emit(False)
908 ################################
910 @Slot(list)
911 def _mpd_changed(self, subsystems = None):
913 Called when MPD signals a change in some subsystems.
915 if not subsystems:
916 subsystems = ['database', 'update', 'stored_playlist', 'playlist', 'output',
917 'player', 'mixer', 'options']
919 if ('player' in subsystems or
920 'mixer' in subsystems or
921 'options' in subsystems):
922 self._command('status', callback = self._update_status)
923 if 'database' in subsystems:
924 self.db_updated.emit()
925 if 'update' in subsystems:
926 pass # just list for completeness
927 if 'stored_playlist' in subsystems:
928 pass
929 if 'playlist' in subsystems:
930 self.playlist_changed.emit()
931 if 'output' in subsystems:
932 self._command('outputs', callback = self._update_outputs)
934 def _update_outputs(self, data):
936 Update outputs states.
938 for output in self._parse_objects(data, ['outputid']):
939 self.outputs[int(output['outputid'])].update(output)
941 def _update_status(self, data):
943 Called when something in status has changed. Check what was it and emit
944 corresponding signals.
946 status = self.status
947 try:
948 self.status = MPDStatus(list(self._parse_objects(data, ''))[0])
949 except IndexError:
950 self.status = MPDStatus()
952 if self.status['state'] == 'play':
953 self._timer.start()
954 else:
955 self._timer.stop()
957 if status['state'] != self.status['state']:
958 self.state_changed.emit(self.status['state'])
959 if status['time'][0] != self.status['time'][0]:
960 self.time_changed.emit(self.status['time'][0])
961 if status['volume'] != self.status['volume']:
962 self.volume_changed.emit(self.status['volume'])
963 if status['repeat'] != self.status['repeat']:
964 self.repeat_changed.emit(self.status['repeat'])
965 if status['random'] != self.status['random']:
966 self.random_changed.emit(self.status['random'])
967 if status['single'] != self.status['single']:
968 self.single_changed.emit(self.status['single'])
969 if status['consume'] != self.status['consume']:
970 self.consume_changed.emit(self.status['consume'])
971 if status['playlist'] != self.status['playlist']:
972 self.playlist_changed.emit()
973 if status['songid'] != self.status['songid']:
974 self._command('currentsong', callback = self._update_cur_song)
976 def _update_cur_song(self, data):
977 try:
978 self.cur_song = Song(list(self._parse_objects(data, ''))[0])
979 except IndexError:
980 self.cur_song = Song()
981 self.song_changed.emit(self.cur_song)
983 def _command(self, *cmd, **kwargs):
985 Send specified command to MPD asynchronously. kwargs must contain
986 a callable 'callback' if the caller want to read a response. Otherwise
987 any reponse from MPD is silently discarded.
989 if not self.is_connected():
990 self._logger.debug('Not connected -- not running command: %s'%cmd[0])
991 if 'callback' in kwargs:
992 kwargs['callback']([])
993 elif not cmd[0] in self._commands:
994 self._logger.error('Command %s not allowed.'%cmd[0])
995 if 'callback' in kwargs:
996 kwargs['callback']([])
997 else:
998 self._socket.write_command(*cmd, **kwargs)
999 def _command_sync(self, *cmd, **kwargs):
1001 Send specified command to MPD synchronously. kwargs must contain
1002 a callable 'parse' used for parsing the reponse.
1004 parse = kwargs['parse']
1006 if not self.is_connected():
1007 self._logger.debug('Not connected -- not running command: %s'%cmd[0])
1008 return parse([])
1009 elif not cmd[0] in self._commands:
1010 self._logger.error('Command %s not allowed.'%cmd[0])
1011 return parse([])
1012 else:
1013 return parse(self._socket.write_command_sync(*cmd))
1015 def _set_output(self, out_id, val):
1017 Enable/disable speciffied output. Called only by AudioOutput.
1019 cmd = 'enableoutput' if val else 'disableoutput'
1020 self._command(cmd, out_id)
1022 def _update_timer(self):
1023 self.status['time'][0] += 1
1024 self.time_changed.emit(self.status['time'][0])
1026 ## MPD output parsing functions ##
1028 def _parse_list(self, data):
1030 Parse a list of 'id_we_dont_care_about: useful_data'.
1032 for line in data:
1033 parts = line.partition(': ')
1034 if not parts[1]:
1035 self._logger.error('Malformed line: %s.'%line)
1036 continue
1037 yield parts[2]
1039 def _parse_objects(self, data, delimiters):
1041 Parse a list of object separated by specified delimiters.
1043 cur = {}
1044 for line in data:
1045 parts = line.partition(': ')
1046 if not parts[1]:
1047 self._logger.error('Malformed line: %s.'%line)
1048 continue
1050 if parts[0] in delimiters and cur:
1051 yield cur
1052 cur = {}
1054 if parts[0] in cur:
1055 cur[parts[0]] += ',' + parts[2]
1056 else:
1057 cur[parts[0]] = parts[2]
1058 if cur:
1059 yield cur
1061 def _parse_songs(self, data):
1063 Parse a list of songs -- output of playlistinfo/listallinfo.
1065 for song in self._parse_objects(data, ['file', 'directory']):
1066 if 'file' in song:
1067 yield Song(song)