# # 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 class MPClient(QtCore.QObject): """This class offers another layer above pympd, with usefull events.""" _client = None _cur_lib = None _cur_playlist = 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} _commands = None _timer_id = None #for querying status changes _db_timer_id = None #for querying db updates _db_update = None #time of last db update _retr_mutex = QtCore.QMutex() logger = None def __init__(self): QtCore.QObject.__init__(self) self._cur_lib = [] self._cur_playlist = [] self._commands = [] self._status = dict(MPClient._status) self.logger = logging.getLogger('mpclient') def connect_mpd(self, host, port, password = None): """Connect to MPD@host:port, optionally using password. Returns True at success, False otherwise.""" self.logger.info('Connecting to MPD...') if self._client: self.logger.warning('Attempted to connect when already connected.') return True try: self._client = mpd.MPDClient() self._client.connect(host, port) except socket.error, e: self.logger.error('Socket error: %s.'%e) self.disconnect_mpd() return False if password: self.password(password) else: self._commands = self._retrieve(self._client.commands) if not self._check_command_ok('listallinfo'): self.logger.error('Don\'t have MPD read permission, diconnecting.') return self.disconnect_mpd() self._update_lib() self._update_playlist() self._update_current_song() self.emit(QtCore.SIGNAL('connected')) #should be removed self.emit(QtCore.SIGNAL('connect_changed'), True) self.logger.info('Successfully connected to MPD.') self._timer_id = self.startTimer(500) self._db_timer_id = self.startTimer(10000) return True def disconnect_mpd(self): """Disconnect from MPD.""" self.logger.info('Disconnecting from MPD...') if self._client: try: self._client.close() self._client.disconnect() except (mpd.ConnectionError, socket.error): pass self._client = None else: self.logger.warning('Attempted to disconnect when not connected.') 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._cur_lib = [] self._cur_playlist = [] self._commands = [] self.emit(QtCore.SIGNAL('disconnected')) #should be removed self.emit(QtCore.SIGNAL('connect_changed'), False) self.logger.info('Disconnected from 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._commands = self._retrieve(self._client.commands) 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 playlist(self): """Returns the current playlist.""" return self._cur_playlist def library(self): """Returns current library.""" return self._cur_lib 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 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) self._client.command_list_end() def outputs(self): """Returns an array of configured MPD audio outputs.""" if self._client: return self._retrieve(self._client.outputs) else: return [] def set_output(self, output_id, state): """Set audio output output_id to state (0/1).""" if not self._check_command_ok('enableoutput'): return if state: self._client.enableoutput(output_id) else: self._client.disableoutput(output_id) 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)) self._client.setvol(volume) def urlhandlers(self): """Returns an array of available url handlers.""" if not self._client: return [] else: return self._client.urlhandlers() def tagtypes(self): """Returns a list of supported tags.""" if not self._check_command_ok('tagtypes'): return [] return self._retrieve(self._client.tagtypes) def commands(self): """List all currently available MPD commands.""" return self._commands def stats(self): """Get MPD statistics.""" return self._retrieve(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, list): """Remove all song IDs in list from the playlist.""" if not self._check_command_ok('deleteid'): return self._client.command_list_ok_begin() for id in list: self.logger.info('Deleting id %d from playlist.'%id) self._client.deleteid(id) self._client.command_list_end() 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): """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) self._client.addid(path.encode('utf-8')) ret = self._client.command_list_end() except mpd.CommandError, e: self.logger.error('Error adding files: %s.'%e) self._update_playlist() if self._status['state'] == 'stop' and ret: self.play(ret[0]) def move(self, source, target): """Move the songs in playlist. Takes a list of source ids and one target position.""" self.logger.info('Moving %d to %d.'%(source, target)) if not self._check_command_ok('moveid'): return self._client.command_list_ok_begin() i = 0 for id in source: self._client.moveid(id, target + i) i += 1 self._client.command_list_end() def _retrieve(self, method): """Makes sure only one call is made at a time to MPD.""" self._retr_mutex.lock() try: ret = method() except socket.error: self.logger.error('Connection to MPD broken.') self._retr_mutex.unlock() self.disconnect_mpd() return None self._retr_mutex.unlock() return ret def _update_lib(self): """Update the cached library.""" self._cur_lib = self._array_to_song_array(self._retrieve(self._client.listallinfo)) id = 0 for song in self._cur_lib: song._data['id'] = id id += 1 def _update_playlist(self): """Update the cached playlist.""" self._cur_playlist = self._array_to_song_array(self._retrieve(self._client.playlistinfo)) def _array_to_song_array(self, array): """Convert an array to an array of Songs.""" return map(lambda entry: Song(entry) , filter(lambda entry: not('directory' in entry), array) ) def _update_current_song(self): """Update the current song.""" song = self._retrieve(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._retrieve(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']) 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.error('Not connected.') if not cmd in self._commands: return self.logger.error('Command %s not accessible'%cmd) return True 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._update_lib() self.emit(QtCore.SIGNAL('db_updated')) return old_status = self._status self._status = self._update_status() if not self._status: return self.disconnect_mpd() self._update_current_song() if self._status['songid'] != old_status['songid']: self.emit(QtCore.SIGNAL('song_changed'), self._status['songid']) if self._status['time'] != old_status['time']: self.emit(QtCore.SIGNAL('time_changed'), self._status['time']) if self._status['state'] != old_status['state']: self.emit(QtCore.SIGNAL('state_changed'), self._status['state']) if self._status['volume'] != old_status['volume']: self.emit(QtCore.SIGNAL('volume_changed'), int(self._status['volume'])) if self._status['repeat'] != old_status['repeat']: self.emit(QtCore.SIGNAL('repeat_changed'), bool(self._status['repeat'])) if self._status['random'] != old_status['random']: self.emit(QtCore.SIGNAL('random_changed'), bool(self._status['random'])) if self._status['single'] != old_status['single']: self.emit(QtCore.SIGNAL('single_changed'), bool(self._status['single'])) if self._status['consume'] != old_status['consume']: self.emit(QtCore.SIGNAL('consume_changed'), bool(self._status['consume'])) if self._status['playlist'] != old_status['playlist']: self._update_playlist() self.emit(QtCore.SIGNAL('playlist_changed'))