# # 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 import mpd import socket import logging from song import Song, SongRef, PlaylistEntryRef 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 # 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): if not self.__check_command_ok('findadd'): return return self._client.findadd(*args) 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.""" 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() try: for path in paths: self.logger.info('Adding %s to playlist'%path) if pos < 0: self._client.addid(path.encode('utf-8')) else: self._client.addid(path.encode('utf-8'), pos) pos += 1 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(str.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: 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)