# # Copyright (C) 2008 jerous # Copyright (C) 2009 Anton Khirnov # # Nephilim is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Nephilim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Nephilim. If not, see . # from PyQt4 import QtCore, QtNetwork from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot import mpd import socket import logging from song import Song, PlaylistEntryRef from mpdsocket import MPDSocket class MPClient(QtCore.QObject): """This class offers another layer above pympd, with usefull events.""" # public, read-only logger = None # these don't change while mpd is running outputs = None tagtypes = None urlhandlers = None commands = None # private __password = None _client = None _cur_song = None _status = {'volume' : 0, 'repeat' : 0, 'random' : 0, 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0, 'time' : 0, 'length' : 0, 'xfade' : 0, 'state' : 'stop', 'single' : 0, 'consume' : 0} _timer_id = None #for querying status changes _db_timer_id = None #for querying db updates _db_update = None #time of last db update __stats = {'artists': '0', 'albums' : '0', 'songs' : '0', 'uptime' : '0', 'playtime' : '0', 'db_playtime' : '0', 'db_update' : '0'} # SIGNALS connect_changed = QtCore.pyqtSignal(bool) db_updated = QtCore.pyqtSignal() song_changed = QtCore.pyqtSignal(object) time_changed = QtCore.pyqtSignal(int) state_changed = QtCore.pyqtSignal(str) volume_changed = QtCore.pyqtSignal(int) repeat_changed = QtCore.pyqtSignal(bool) random_changed = QtCore.pyqtSignal(bool) single_changed = QtCore.pyqtSignal(bool) consume_changed = QtCore.pyqtSignal(bool) playlist_changed = QtCore.pyqtSignal() def __init__(self): QtCore.QObject.__init__(self) self.logger = logging.getLogger('mpclient') self.__update_static() self._status = dict(MPClient._status) def connect_mpd(self, host, port, password = None): """Connect to MPD@host:port, optionally using password.""" self.logger.info('Connecting to MPD...') if self._client: self.logger.warning('Attempted to connect when already connected.') return self._client = mpd.MPDClient() self._client.connect_changed.connect(lambda val:self.__finish_connect() if val else self.__finish_disconnect()) self._client.connect_mpd(host, port) self.__password = password def disconnect_mpd(self): """Disconnect from MPD.""" self.logger.info('Disconnecting from MPD...') if self._client: self._client.disconnect_mpd() def password(self, password): """Use the password to authenticate with MPD.""" self.logger.info('Authenticating with MPD.') if not self.__check_command_ok('password'): return try: self._client.password(password) self.logger.info('Successfully authenticated') self.__update_static() except mpd.CommandError: self.logger.error('Incorrect MPD password.') def is_connected(self): """Returns True if connected to MPD, False otherwise.""" return self._client != None def status(self): """Get current MPD status.""" return self._status def playlistinfo(self): """Returns a list of songs in current playlist.""" self.logger.info('Listing current playlist.') if not self.__check_command_ok('playlistinfo'): raise StopIteration for song in self._client.playlistinfo(): yield Song(song) raise StopIteration def library(self): """Returns a list of all songs in library.""" self.logger.info('Listing library.') if not self.__check_command_ok('listallinfo'): raise StopIteration for song in self._client.listallinfo(): if 'file' in song: yield Song(song) raise StopIteration def current_song(self): """Returns the current playing song.""" return self._cur_song def is_playing(self): """Returns True if MPD is playing, False otherwise.""" return self._status['state'] == 'play' def find(self, *args): if not self.__check_command_ok('find'): raise StopIteration for song in self._client.find(*args): yield Song(song) raise StopIteration def findadd(self, *args): """Find tracks with given tags and add them to playlist. Takes a list of (tag, value).""" self.logger.info('Findadd %s.'%unicode(args)) if not self.__check_command_ok('findadd'): return return self._client.findadd(*args) def playlistid(self, plid): """Return a song with a given playlist id.""" self.logger.info('Getting id %s.'%('of id %s'%(plid) if plid else '')) if not self.__check_command_ok('play'): return ret = None for it in self._client.playlistid(plid): ret = Song(it) return ret def update_db(self, paths = None): """Starts MPD database update.""" self.logger.info('Updating database %s'%(paths if paths else '.')) if not self.__check_command_ok('update'): return if not paths: return self._client.update() self._client.command_list_ok_begin() for path in paths: self._client.update(path) list(self._client.command_list_end()) def volume(self): """Get current volume.""" return int(self._status['volume']) def set_volume(self, volume): """Set volume to volume.""" self.logger.info('Setting volume to %d.'%volume) if not self.__check_command_ok('setvol'): return volume = min(100, max(0, volume)) try: self._client.setvol(volume) except mpd.CommandError, e: self.logger.warning('Error setting volume (probably no outputs enabled): %s.'%e) def stats(self): """Get MPD statistics.""" if not self.__check_command_ok('stats'): return self.__stats return self._client.stats() def repeat(self, val): """Set repeat playlist to val (True/False).""" self.logger.info('Setting repeat to %d.'%val) if not self.__check_command_ok('repeat'): return if isinstance(val, bool): val = 1 if val else 0 self._client.repeat(val) def random(self, val): """Set random playback to val (True, False).""" self.logger.info('Setting random to %d.'%val) if not self.__check_command_ok('random'): return if isinstance(val, bool): val = 1 if val else 0 self._client.random(val) def crossfade(self, time): """Set crossfading between songs.""" self.logger.info('Setting crossfade to %d'%time) if not self.__check_command_ok('crossfade'): return self._client.crossfade(time) def single(self, val): """Set single playback to val (True, False)""" self.logger.info('Setting single to %d.'%val) if not self.__check_command_ok('single'): return if isinstance(val, bool): val = 1 if val else 0 self._client.single(val) def consume(self, val): """Set consume mode to val (True, False)""" self.logger.info('Setting consume to %d.'%val) if not self.__check_command_ok('consume'): return if isinstance(val, bool): val = 1 if val else 0 self._client.consume(val) def play(self, id = None): """Play song with ID id or next song if id is None.""" self.logger.info('Starting playback %s.'%('of id %s'%(id) if id else '')) if not self.__check_command_ok('play'): return if id: self._client.playid(id) else: self._client.playid() def pause(self): """Pause playing.""" self.logger.info('Pausing playback.') if not self.__check_command_ok('pause'): return self._client.pause(1) def resume(self): """Resume playing.""" self.logger.info('Resuming playback.') if not self.__check_command_ok('pause'): return self._client.pause(0) def next(self): """Move on to the next song in the playlist.""" self.logger.info('Skipping to next song.') if not self.__check_command_ok('next'): return self._client.next() def previous(self): """Move back to the previous song in the playlist.""" self.logger.info('Moving to previous song.') if not self.__check_command_ok('previous'): return self._client.previous() def stop(self): """Stop playing.""" self.logger.info('Stopping playback.') if not self.__check_command_ok('stop'): return self._client.stop() def seek(self, time): """Seek to time (in seconds).""" self.logger.info('Seeking to %d.'%time) if not self.__check_command_ok('seekid'): return if self._status['songid'] > 0: self._client.seekid(self._status['songid'], time) def delete(self, ids): """Remove all song IDs in list from the playlist.""" if not self.__check_command_ok('deleteid'): return self._client.command_list_ok_begin() try: for id in ids: self.logger.info('Deleting id %s from playlist.'%id) self._client.deleteid(id) list(self._client.command_list_end()) except mpd.CommandError, e: self.logger.error('Error deleting files: %s.'%e) def clear(self): """Clear current playlist.""" self.logger.info('Clearing playlist.') if not self.__check_command_ok('clear'): return self._client.clear() def add(self, paths, pos = -1): """Add all files in paths to the current playlist.""" if not self.__check_command_ok('addid'): return ret = None self._client.command_list_ok_begin() for path in paths: self.logger.info('Adding %s to playlist'%path) if pos < 0: self._client.addid(path) else: self._client.addid(path, pos) pos += 1 try: ret = list(self._client.command_list_end()) except mpd.CommandError, e: self.logger.error('Error adding files: %s.'%e) if self._status['state'] == 'stop' and ret: self.play(ret[0]) def move(self, source, target): """Move the songs in playlist. Takes one source id and one target position.""" self.logger.info('Moving %s to %s.'%(source, target)) if not self.__check_command_ok('moveid'): return self._client.moveid(source, target) #### private #### def __finish_connect(self): if self.__password: self.password(self.__password) else: self.__update_static() if not self.__check_command_ok('listallinfo'): self.logger.error('Don\'t have MPD read permission, diconnecting.') return self.disconnect_mpd() self.__update_current_song() self._db_update = self.stats()['db_update'] self.connect_changed.emit(True) self.logger.info('Successfully connected to MPD.') self._timer_id = self.startTimer(500) self._db_timer_id = self.startTimer(1000) def __finish_disconnect(self): self._client = None if self._timer_id: self.killTimer(self._timer_id) self._timer_id = None if self._db_timer_id: self.killTimer(self._db_timer_id) self._db_timer_id = None self._status = dict(MPClient._status) self._cur_song = None self.__update_static() self.connect_changed.emit(False) self.logger.info('Disconnected from MPD.') def __update_current_song(self): """Update the current song.""" song = self._client.currentsong() if not song: self._cur_song = None else: self._cur_song = Song(song) def _update_status(self): """Get current status""" if not self._client: return None ret = self._client.status() if not ret: return None ret['repeat'] = int(ret['repeat']) ret['random'] = int(ret['random']) ret['single'] = int(ret['single']) ret['consume'] = int(ret['consume']) ret['volume'] = int(ret['volume']) if 'time' in ret: cur, len = ret['time'].split(':') ret['length'] = int(len) ret['time'] = int(cur) else: ret['length'] = 0 ret['time'] = 0 if not 'songid' in ret: ret['songid'] = '-1' return ret def __check_command_ok(self, cmd): if not self._client: return self.logger.info('Not connected.') if not cmd in self.commands: return self.logger.error('Command %s not accessible'%cmd) return True def __update_static(self): """Update static values, called on connect/disconnect.""" if self._client: self.commands = list(self._client.commands()) else: self.commands = [] if self.__check_command_ok('outputs'): outputs = [] for output in self._client.outputs(): outputs.append(AudioOutput(self, output['outputname'], output['outputid'], bool(output['outputenabled']))) self.outputs = outputs else: self.outputs = [] if self.__check_command_ok('tagtypes'): self.tagtypes = map(unicode.lower, self._client.tagtypes()) + ['file'] else: self.tagtypes = [] if self.__check_command_ok('urlhandlers'): self.urlhandlers = list(self._client.urlhandlers()) else: self.urlhandlers = [] def set_output(self, output_id, state): """Set audio output output_id to state (0/1). Called only by AudioOutput.""" if not self.__check_command_ok('enableoutput'): return if state: self._client.enableoutput(output_id) else: self._client.disableoutput(output_id) def timerEvent(self, event): """Check for changes since last check.""" if event.timerId() == self._db_timer_id: #timer for monitoring db changes db_update = self.stats()['db_update'] if db_update > self._db_update: self.logger.info('Database updated.') self._db_update = db_update self.db_updated.emit() return old_status = self._status self._status = self._update_status() if not self._status: self.logger.error('Error reading status.') return self.disconnect_mpd() if self._status['songid'] != old_status['songid']: self.__update_current_song() self.song_changed.emit(PlaylistEntryRef(self, self._status['songid'])) if self._status['time'] != old_status['time']: self.time_changed.emit(self._status['time']) if self._status['state'] != old_status['state']: self.state_changed.emit(self._status['state']) if self._status['volume'] != old_status['volume']: self.volume_changed.emit( int(self._status['volume'])) if self._status['repeat'] != old_status['repeat']: self.repeat_changed.emit(bool(self._status['repeat'])) if self._status['random'] != old_status['random']: self.random_changed.emit(bool(self._status['random'])) if self._status['single'] != old_status['single']: self.single_changed.emit(bool(self._status['single'])) if self._status['consume'] != old_status['consume']: self.consume_changed.emit(bool(self._status['consume'])) if self._status['playlist'] != old_status['playlist']: self.playlist_changed.emit() outputs = list(self._client.outputs()) for i in range(len(outputs)): if int(outputs[i]['outputenabled']) != int(self.outputs[i].state): self.outputs[i].mpd_toggle_state() class AudioOutput(QtCore.QObject): """This class represents an MPD audio output.""" # public, const mpclient = None name = None id = None state = None # SIGNALS state_changed = QtCore.pyqtSignal(bool) #### public #### def __init__(self, mpclient, name, id, state): QtCore.QObject.__init__(self) self.mpclient = mpclient self.name = name self.id = id self.state = state @QtCore.pyqtSlot(bool) def set_state(self, state): self.mpclient.set_output(self.id, state) #### private #### def mpd_toggle_state(self): """This is called by mpclient to inform about output state change.""" self.state = not self.state self.state_changed.emit(self.state) class AudioOutput2(QtCore.QObject): """This class represents an MPD audio output.""" #### PUBLIC #### # constants name = None # read-only state = None # SIGNALS state_changed = QtCore.pyqtSignal(bool) #### public #### def __init__(self, data, set_state, parent = None): QtCore.QObject.__init__(self, parent) self.name = data['outputname'] self.state = int(data['outputenabled']) self.set_state = set_state @Slot(bool) def set_state(self, state): pass def update(self, data): """ This is called by mpclient to inform about output state change. """ if int(data['outputenabled']) != self.state: self.state = not self.state self.state_changed.emit(self.state) class MPDStatus(dict): _status = {'volume' : 0, 'repeat' : 0, 'single' : 0, 'consume' : 0, 'playlist' : '-1', 'playlistlength' : 0, 'state' : 'stop', 'song' : -1, 'songid' : '-1', 'nextsong' : -1, 'nextsongid' : '-1', 'time' : '0:0', 'elapsed' : .0, 'bitrate' : 0, 'xfade' : 0, 'mixrampdb' : .0, 'mixrampdelay' : .0, 'audio' : '0:0:0', 'updatings_db' : -1, 'error' : '', 'random' : 0 } def __init__(self, data = {}): dict.__init__(self, MPDStatus._status) for key in data: if key in self._status: self[key] = type(self._status[key])(data[key]) else: self[key] = data[key] try: self['time'] = map(int, self['time'].split(':')) except ValueError: self['time'] = [0, 0] try: self['audio'] = tuple(map(int, self['audio'].split(':'))) except ValueError: self['audio'] = (0, 0, 0) class MPClient2(QtCore.QObject): """ A high-level MPD interface. It is mostly asynchronous -- all responses from MPD are read via callbacks. A callback may be None, in which case the data is silently discarded. Callbacks that take iterators must ensure that the iterator is exhausted. """ #### PUBLIC #### # these don't change while we are connected """A list of AudioOutputs available.""" outputs = None """A list of supported tags (valid indices for Song).""" tagtypes = None """A list of supported URL handlers.""" urlhandlers = None # read-only """An MPDStatus object representing current status.""" status = None """A Song object representing current song.""" cur_song = None # SIGNALS connect_changed = Signal(bool) db_updated = Signal() time_changed = Signal(int) song_changed = Signal(object) state_changed = Signal(str) volume_changed = Signal(int) repeat_changed = Signal(bool) random_changed = Signal(bool) single_changed = Signal(bool) consume_changed = Signal(bool) playlist_changed = Signal() #### PRIVATE #### # const _sup_ver = (0, 16, 0) _logger = None _timer = None # these don't change while we are connected _commands = None _socket = None _password = None #### PUBLIC #### def __init__(self, parent = None): QtCore.QObject.__init__(self, parent) self._logger = logging.getLogger('%smpclient'%(unicode(parent) + "." if parent else "")) self._timer = QtCore.QTimer(self) self._timer.setInterval(1000) self._timer.timeout.connect(self._update_timer) self._socket = MPDSocket(self) self._commands = [] self.status = MPDStatus() self.cur_song = Song() self.outputs = [] self.urlhandlers = [] self.tagtypes = [] def __str__(self): return self._logger.name def connect_mpd(self, host = "localhost", port = 6600, password = None): """ Connect to MPD at host:port optionally using a password. A Unix domain socket is used if port is omitted. """ self._logger.info('Connecting to MPD...') if self.is_connected(): self._logger.warning('Already connected.') self._socket.connect_changed.connect(lambda val: self._handle_connected() if val else self._handle_disconnected()) self._socket.connect_mpd(host, port) self._password = password def disconnect_mpd(self): """ Disconnect from MPD. """ self._logger.info('Disconnecting from MPD.') if self.is_connected(): self._socket.write_command('close') def is_connected(self): """ Returns True if connected to MPD, False otherwise. """ return self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState def playlist(self, callback): """ Request current playlist from MPD. Callback will be called with an iterator over Songs in current playlist as the argument. """ self._command('playlistinfo', callback = lambda data: callback(self._parse_songs(data))) def database(self, callback): """ Request database information from MPD. Callback will be called with an iterator over all Songs in the database as the argument. """ self._command('listallinfo', callback = lambda data: callback(self._parse_songs(data))) def find(self, callback, *args): """ Request a search on MPD. Callback will be called with an iterator over all found songs. For allowed values of args, see MPD protocol documentation. """ self._command('find', args, callback = lambda data: callback(self._parse_songs(data))) def find_sync(self, *args): """ Search for songs on MPD synchronously. Returns an iterator over all found songs. For allowed values of args, see MPD protocol documentation. """ return self._command_sync('find', args, parse = lambda data: self._parse_songs(data)) def findadd(self, *args): """ Request a search on MPD and add found songs to current playlist. Allowed values of args are same as for find. """ self._command('findadd', *args) def get_plist_song(self, plid): """ Get a song with a given playlist id synchronously. """ return self._command_sync('playlistid', plid, parse = lambda data: Song(list(self._parse_objects(data, []))[0])) def set_volume(self, volume): """ Set MPD volume level. """ volume = min(100, max(0, volume)) self._command('setvol', volume) def repeat(self, val): """ Enable/disable repeat. """ val = '1' if val else '0' self._command('repeat', val) def random(self, val): """ Enable/disable random. """ val = '1' if val else '0' self._command('random', val) def consume(self, val): """ Enable/disable consume. """ val = '1' if val else '0' self._command('consume', val) def single(self, val): """ Enable/disable single. """ val = '1' if val else '0' self._command('single', val) def crossfade(self, time): """ Set crossfade to specified time. """ self._command('crossfade', val) def play(self, id = None): """ Start playback of song with a specified id. If no id is given, then start on current song/beginning. """ args = ['playid'] if id: args.append(id) self._command(*args) def pause(self): """ Pause playback. """ self._command('pause', 1) def resume(self): """ Resume paused playback. """ self._command('pause', 0) def next(self): """ Move on to next song. """ self._command('next') def previous(self): """ Move back to previous song. """ self._command('previous') def stop(self): """ Stop playback. """ self._command('stop') def seek(self, time): """ Seek to specified time in current song. """ self._command('seekid', self.status['songid'], time) def delete(self, ids): """ Delete songs with specified ids from playlist. """ for id in ids: self._command('deleteid', id) def clear(self): """ Clear current playlist. """ self._command('clear') def add(self, paths, pos = -1): """ Add specified songs to specified position in current playlist. """ # start playback of the first added song if MPD is stopped if self.status['state'] == 'stop': cb = lambda data: [self.play(sid) for sid in self._parse_list(data) ] else: cb = None args = ['addid', ''] if pos >= 0: args.append(pos) for path in paths: args[1] = path if cb: self._command(*args, callback = cb) cb = None else: self._command(*args) if pos >= 0: args[2] += 1 def move(self, src, dst): """ Move a song with given src id to position dst. """ self._command('moveid', src, dst) #### PRIVATE #### ## connection functions ## # these functions are called during connection process # # XXX: maybe use a generator? @Slot() def _handle_connected(self): """ Called when a connection is established. Send a password and start getting locally stored values. """ self._logger.debug('Connection established.') # check if protocol version is supported v = self._socket.version if v[0] != self._sup_ver[0]: self._logger.error('Server reported unsupported major protocol version %d, disconnecting.'%v[0]) return self.disconnect_mpd() if v[1] < self._sup_ver[1]: self._logger.warning('Server reported too low minor protocol version %d. Continuing, but things might break.'%v[1]) if self._password: self._socket.write_command('password', self._password) self._socket.write_command('commands', callback = self._parse_commands) def _parse_commands(self, data): """ Receive a list of available commands and update the other locally stored values. """ self._logger.debug('Receiving command list.') self._commands = list(self._parse_list(data)) if not 'listallinfo' in self._commands: self._logger.error('Don\'t have MPD read permission, diconnecting.') return self.disconnect_mpd() # update cached values self._command('outputs', callback = self._parse_outputs) self._command('tagtypes', callback = self._parse_tagtypes) self._command('urlhandlers', callback = self._parse_urlhandlers) def _parse_outputs(self, data): """ Update a list of outputs. """ self._logger.debug('Receiving outputs.') self.outputs = [] for output in self._parse_objects(data, ['outputid']): self.outputs.append(AudioOutput(output, lambda val, outid = output['outputid']: self._set_output(outid, val), self)) def _parse_tagtypes(self, data): """ Update a list of tag types. """ self._logger.debug('Receiving tag types.') self.tagtypes = list(self._parse_list(data)) + ['file'] def _parse_urlhandlers(self, data): """ Update a list of URL handlers and finish connection. """ self._logger.debug('Receiving URL handlers.') self.urlhandlers = list(self._parse_list(data)) # done initializing data, finish connecting return self._finish_connect() def _finish_connect(self): """ Called when connecting is completely done. Emit all signals. """ self._logger.info('Successfully connected to MPD.') self._socket.subsystems_changed.connect(self._mpd_changed) self.connect_changed.emit(True) self._mpd_changed() @Slot() def _handle_disconnected(self): """ Called when connection is closed. Clear all cached data and emit corresponding signals. """ self._logger.info('Disconnected from MPD.') self._commands = [] self.outputs = {} self.tagtypes = [] self.urlhandlers = [] self._mpd_changed() self.connect_changed.emit(False) ################################ @Slot(list) def _mpd_changed(self, subsystems = None): """ Called when MPD signals a change in some subsystems. """ if not subsystems: subsystems = ['database', 'update', 'stored_playlist', 'playlist', 'output', 'player', 'mixer', 'options'] if ('player' in subsystems or 'mixer' in subsystems or 'options' in subsystems): self._command('status', callback = self._update_status) if 'database' in subsystems: self.db_updated.emit() if 'update' in subsystems: pass # just list for completeness if 'stored_playlist' in subsystems: pass if 'playlist' in subsystems: self.playlist_changed.emit() if 'output' in subsystems: self._command('outputs', callback = self._update_outputs) def _update_outputs(self, data): """ Update outputs states. """ for output in self._parse_objects(data, ['outputid']): self.outputs[int(output['outputid'])].update(output) def _update_status(self, data): """ Called when something in status has changed. Check what was it and emit corresponding signals. """ status = self.status try: self.status = MPDStatus(list(self._parse_objects(data, ''))[0]) except IndexError: self.status = MPDStatus() if self.status['state'] == 'play': self._timer.start() else: self._timer.stop() if status['state'] != self.status['state']: self.state_changed.emit(self.status['state']) if status['time'][0] != self.status['time'][0]: self.time_changed.emit(self.status['time'][0]) if status['volume'] != self.status['volume']: self.volume_changed.emit(self.status['volume']) if status['repeat'] != self.status['repeat']: self.repeat_changed.emit(self.status['repeat']) if status['random'] != self.status['random']: self.random_changed.emit(self.status['random']) if status['single'] != self.status['single']: self.single_changed.emit(self.status['single']) if status['consume'] != self.status['consume']: self.consume_changed.emit(self.status['consume']) if status['playlist'] != self.status['playlist']: self.playlist_changed.emit() if status['songid'] != self.status['songid']: self._command('currentsong', callback = self._update_cur_song) def _update_cur_song(self, data): try: self.cur_song = Song(list(self._parse_objects(data, ''))[0]) except IndexError: self.cur_song = Song() self.song_changed.emit(self.cur_song) def _command(self, *cmd, **kwargs): """ Send specified command to MPD asynchronously. kwargs must contain a callable 'callback' if the caller want to read a response. Otherwise any reponse from MPD is silently discarded. """ if not self.is_connected(): self._logger.debug('Not connected -- not running command: %s'%cmd[0]) if 'callback' in kwargs: kwargs['callback']([]) elif not cmd[0] in self._commands: self._logger.error('Command %s not allowed.'%cmd[0]) if 'callback' in kwargs: kwargs['callback']([]) else: self._socket.write_command(*cmd, **kwargs) def _command_sync(self, *cmd, **kwargs): """ Send specified command to MPD synchronously. kwargs must contain a callable 'parse' used for parsing the reponse. """ parse = kwargs['parse'] if not self.is_connected(): self._logger.debug('Not connected -- not running command: %s'%cmd[0]) return parse([]) elif not cmd[0] in self._commands: self._logger.error('Command %s not allowed.'%cmd[0]) return parse([]) else: return parse(self._socket.write_command_sync(*cmd)) def _set_output(self, out_id, val): """ Enable/disable speciffied output. Called only by AudioOutput. """ cmd = 'enableoutput' if val else 'disableoutput' self._command(cmd, out_id) def _update_timer(self): self.status['time'][0] += 1 self.time_changed.emit(self.status['time'][0]) ## MPD output parsing functions ## def _parse_list(self, data): """ Parse a list of 'id_we_dont_care_about: useful_data'. """ for line in data: parts = line.partition(': ') if not parts[1]: self._logger.error('Malformed line: %s.'%line) continue yield parts[2] def _parse_objects(self, data, delimiters): """ Parse a list of object separated by specified delimiters. """ cur = {} for line in data: parts = line.partition(': ') if not parts[1]: self._logger.error('Malformed line: %s.'%line) continue if parts[0] in delimiters and cur: yield cur cur = {} if parts[0] in cur: cur[parts[0]] += ',' + parts[2] else: cur[parts[0]] = parts[2] if cur: yield cur def _parse_songs(self, data): """ Parse a list of songs -- output of playlistinfo/listallinfo. """ for song in self._parse_objects(data, ['file', 'directory']): if 'file' in song: yield Song(song)