mpclient: don't cache library and playlist
[nephilim.git] / nephilim / mpclient.py
bloba1298b14d2b3d798bea297a9abf41e05ddf885a4
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
26 class MPClient(QtCore.QObject):
27 """This class offers another layer above pympd, with usefull events."""
28 _client = None
29 _cur_song = None
30 _status = {'volume' : 0, 'repeat' : 0, 'random' : 0,
31 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
32 'time' : 0, 'length' : 0, 'xfade' : 0,
33 'state' : 'stop', 'single' : 0,
34 'consume' : 0}
35 _commands = None
37 _timer_id = None #for querying status changes
38 _db_timer_id = None #for querying db updates
39 _db_update = None #time of last db update
41 _retr_mutex = QtCore.QMutex()
43 logger = None
45 # SIGNALS
46 connect_changed = QtCore.pyqtSignal(bool)
47 db_updated = QtCore.pyqtSignal()
48 song_changed = QtCore.pyqtSignal(str)
49 time_changed = QtCore.pyqtSignal(int)
50 state_changed = QtCore.pyqtSignal(str)
51 volume_changed = QtCore.pyqtSignal(int)
52 repeat_changed = QtCore.pyqtSignal(bool)
53 random_changed = QtCore.pyqtSignal(bool)
54 single_changed = QtCore.pyqtSignal(bool)
55 consume_changed = QtCore.pyqtSignal(bool)
56 playlist_changed = QtCore.pyqtSignal()
58 def __init__(self):
59 QtCore.QObject.__init__(self)
60 self._commands = []
61 self._status = dict(MPClient._status)
62 self.logger = logging.getLogger('mpclient')
64 def connect_mpd(self, host, port, password = None):
65 """Connect to MPD@host:port, optionally using password.
66 Returns True at success, False otherwise."""
68 self.logger.info('Connecting to MPD...')
69 if self._client:
70 self.logger.warning('Attempted to connect when already connected.')
71 return True
73 try:
74 self._client = mpd.MPDClient()
75 self._client.connect(host, port)
76 except socket.error, e:
77 self.logger.error('Socket error: %s.'%e)
78 self.disconnect_mpd()
79 return False
81 if password:
82 self.password(password)
83 else:
84 self._commands = self._retrieve(self._client.commands)
86 if not self.__check_command_ok('listallinfo'):
87 self.logger.error('Don\'t have MPD read permission, diconnecting.')
88 return self.disconnect_mpd()
90 self._update_current_song()
91 self._db_update = self.stats()['db_update']
93 self.emit(QtCore.SIGNAL('connected')) #should be removed
94 self.connect_changed.emit(True)
95 self.logger.info('Successfully connected to MPD.')
96 self._timer_id = self.startTimer(500)
97 self._db_timer_id = self.startTimer(10000)
98 return True
99 def disconnect_mpd(self):
100 """Disconnect from MPD."""
101 self.logger.info('Disconnecting from MPD...')
102 if self._client:
103 try:
104 self._client.close()
105 self._client.disconnect()
106 except (mpd.ConnectionError, socket.error):
107 pass
108 self._client = None
109 else:
110 self.logger.warning('Attempted to disconnect when not connected.')
112 if self._timer_id:
113 self.killTimer(self._timer_id)
114 self._timer_id = None
115 if self._db_timer_id:
116 self.killTimer(self._db_timer_id)
117 self._db_timer_id = None
118 self._status = dict(MPClient._status)
119 self._cur_song = None
120 self._commands = []
121 self.emit(QtCore.SIGNAL('disconnected')) #should be removed
122 self.connect_changed.emit(False)
123 self.logger.info('Disconnected from MPD.')
124 def password(self, password):
125 """Use the password to authenticate with MPD."""
126 self.logger.info('Authenticating with MPD.')
127 if not self.__check_command_ok('password'):
128 return
129 try:
130 self._client.password(password)
131 self.logger.info('Successfully authenticated')
132 self._commands = self._retrieve(self._client.commands)
133 except mpd.CommandError:
134 self.logger.error('Incorrect MPD password.')
135 def is_connected(self):
136 """Returns True if connected to MPD, False otherwise."""
137 return self._client != None
139 def status(self):
140 """Get current MPD status."""
141 return self._status
142 def playlist(self):
143 """Returns a list of songs in current playlist."""
144 self.logger.info('Listing current playlist.')
145 if not self.__check_command_ok('playlistinfo'):
146 return []
147 return self._array_to_song_array(self._retrieve(self._client.playlistinfo))
148 def library(self):
149 """Returns a list of all songs in library."""
150 self.logger.info('Listing library.')
151 if not self.__check_command_ok('listallinfo'):
152 return []
153 return self._array_to_song_array(self._retrieve(self._client.listallinfo))
154 def current_song(self):
155 """Returns the current playing song."""
156 return self._cur_song
157 def is_playing(self):
158 """Returns True if MPD is playing, False otherwise."""
159 return self._status['state'] == 'play'
160 def find(self, path):
161 if not self.__check_command_ok('find'):
162 return []
163 return self._client.find('file', path.encode('utf-8'))
165 def update_db(self, paths = None):
166 """Starts MPD database update."""
167 self.logger.info('Updating database %s'%(paths if paths else '.'))
168 if not self.__check_command_ok('update'):
169 return
170 if not paths:
171 return self._client.update()
172 self._client.command_list_ok_begin()
173 for path in paths:
174 self._client.update(path)
175 self._client.command_list_end()
177 def outputs(self):
178 """Returns an array of configured MPD audio outputs."""
179 if self._client:
180 return self._retrieve(self._client.outputs)
181 else:
182 return []
183 def set_output(self, output_id, state):
184 """Set audio output output_id to state (0/1)."""
185 if not self.__check_command_ok('enableoutput'):
186 return
187 if state:
188 self._client.enableoutput(output_id)
189 else:
190 self._client.disableoutput(output_id)
192 def volume(self):
193 """Get current volume."""
194 return int(self._status['volume'])
195 def set_volume(self, volume):
196 """Set volume to volume."""
197 self.logger.info('Setting volume to %d.'%volume)
198 if not self.__check_command_ok('setvol'):
199 return
200 volume = min(100, max(0, volume))
201 self._client.setvol(volume)
203 def urlhandlers(self):
204 """Returns an array of available url handlers."""
205 if not self._client:
206 return []
207 else:
208 return self._client.urlhandlers()
209 def tagtypes(self):
210 """Returns a list of supported tags."""
211 if not self.__check_command_ok('tagtypes'):
212 return []
214 return self._retrieve(self._client.tagtypes)
215 def commands(self):
216 """List all currently available MPD commands."""
217 return self._commands
218 def stats(self):
219 """Get MPD statistics."""
220 return self._retrieve(self._client.stats)
222 def repeat(self, val):
223 """Set repeat playlist to val (True/False)."""
224 self.logger.info('Setting repeat to %d.'%val)
225 if not self.__check_command_ok('repeat'):
226 return
227 if isinstance(val, bool):
228 val = 1 if val else 0
229 self._client.repeat(val)
230 def random(self, val):
231 """Set random playback to val (True, False)."""
232 self.logger.info('Setting random to %d.'%val)
233 if not self.__check_command_ok('random'):
234 return
235 if isinstance(val, bool):
236 val = 1 if val else 0
237 self._client.random(val)
238 def crossfade(self, time):
239 """Set crossfading between songs."""
240 self.logger.info('Setting crossfade to %d'%time)
241 if not self.__check_command_ok('crossfade'):
242 return
243 self._client.crossfade(time)
244 def single(self, val):
245 """Set single playback to val (True, False)"""
246 self.logger.info('Setting single to %d.'%val)
247 if not self.__check_command_ok('single'):
248 return
249 if isinstance(val, bool):
250 val = 1 if val else 0
251 self._client.single(val)
252 def consume(self, val):
253 """Set consume mode to val (True, False)"""
254 self.logger.info('Setting consume to %d.'%val)
255 if not self.__check_command_ok('consume'):
256 return
257 if isinstance(val, bool):
258 val = 1 if val else 0
259 self._client.consume(val)
261 def play(self, id = None):
262 """Play song with ID id or next song if id is None."""
263 self.logger.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
264 if not self.__check_command_ok('play'):
265 return
266 if id:
267 self._client.playid(id)
268 else:
269 self._client.playid()
270 def pause(self):
271 """Pause playing."""
272 self.logger.info('Pausing playback.')
273 if not self.__check_command_ok('pause'):
274 return
275 self._client.pause(1)
276 def resume(self):
277 """Resume playing."""
278 self.logger.info('Resuming playback.')
279 if not self.__check_command_ok('pause'):
280 return
281 self._client.pause(0)
282 def next(self):
283 """Move on to the next song in the playlist."""
284 self.logger.info('Skipping to next song.')
285 if not self.__check_command_ok('next'):
286 return
287 self._client.next()
288 def previous(self):
289 """Move back to the previous song in the playlist."""
290 self.logger.info('Moving to previous song.')
291 if not self.__check_command_ok('previous'):
292 return
293 self._client.previous()
294 def stop(self):
295 """Stop playing."""
296 self.logger.info('Stopping playback.')
297 if not self.__check_command_ok('stop'):
298 return
299 self._client.stop()
300 def seek(self, time):
301 """Seek to time (in seconds)."""
302 self.logger.info('Seeking to %d.'%time)
303 if not self.__check_command_ok('seekid'):
304 return
305 if self._status['songid'] > 0:
306 self._client.seekid(self._status['songid'], time)
308 def delete(self, list):
309 """Remove all song IDs in list from the playlist."""
310 if not self.__check_command_ok('deleteid'):
311 return
312 self._client.command_list_ok_begin()
313 try:
314 for id in list:
315 self.logger.info('Deleting id %s from playlist.'%id)
316 self._client.deleteid(id)
317 self._client.command_list_end()
318 except mpd.CommandError, e:
319 self.logger.error('Error deleting files: %s.'%e)
320 def clear(self):
321 """Clear current playlist."""
322 self.logger.info('Clearing playlist.')
323 if not self.__check_command_ok('clear'):
324 return
325 self._client.clear()
326 def add(self, paths):
327 """Add all files in paths to the current playlist."""
328 if not self.__check_command_ok('addid'):
329 return
330 ret = None
331 self._client.command_list_ok_begin()
332 try:
333 for path in paths:
334 self.logger.info('Adding %s to playlist'%path)
335 self._client.addid(path.encode('utf-8'))
336 ret = self._client.command_list_end()
337 except mpd.CommandError, e:
338 self.logger.error('Error adding files: %s.'%e)
339 if self._status['state'] == 'stop' and ret:
340 self.play(ret[0])
341 def move(self, source, target):
342 """Move the songs in playlist. Takes a list of source ids and one target position."""
343 self.logger.info('Moving %d to %d.'%(source, target))
344 if not self.__check_command_ok('moveid'):
345 return
346 self._client.command_list_ok_begin()
347 i = 0
348 for id in source:
349 self._client.moveid(id, target + i)
350 i += 1
351 self._client.command_list_end()
353 def _retrieve(self, method):
354 """Makes sure only one call is made at a time to MPD."""
355 self._retr_mutex.lock()
356 try:
357 ret = method()
358 except socket.error:
359 self.logger.error('Connection to MPD broken.')
360 self._retr_mutex.unlock()
361 self.disconnect_mpd()
362 return None
364 self._retr_mutex.unlock()
365 return ret
366 def _array_to_song_array(self, array):
367 """Convert an array to an array of Songs."""
368 return map(lambda entry: Song(entry)
369 , filter(lambda entry: not('directory' in entry), array)
371 def _update_current_song(self):
372 """Update the current song."""
373 song = self._retrieve(self._client.currentsong)
374 if not song:
375 self._cur_song = None
376 else:
377 self._cur_song = Song(song)
378 def _update_status(self):
379 """Get current status"""
380 if not self._client:
381 return None
382 ret = self._retrieve(self._client.status)
383 if not ret:
384 return None
386 ret['repeat'] = int(ret['repeat'])
387 ret['random'] = int(ret['random'])
388 ret['single'] = int(ret['single'])
389 ret['consume'] = int(ret['consume'])
390 if 'time' in ret:
391 cur, len = ret['time'].split(':')
392 ret['length'] = int(len)
393 ret['time'] = int(cur)
394 else:
395 ret['length'] = 0
396 ret['time'] = 0
398 if not 'songid' in ret:
399 ret['songid'] = '-1'
401 return ret
402 def __check_command_ok(self, cmd):
403 if not self._client:
404 return self.logger.info('Not connected.')
405 if not cmd in self._commands:
406 return self.logger.error('Command %s not accessible'%cmd)
407 return True
409 def timerEvent(self, event):
410 """Check for changes since last check."""
411 if event.timerId() == self._db_timer_id:
412 #timer for monitoring db changes
413 db_update = self.stats()['db_update']
414 if db_update > self._db_update:
415 self.logger.info('Database updated.')
416 self._db_update = db_update
417 self.db_updated.emit()
418 return
421 old_status = self._status
422 self._status = self._update_status()
424 if not self._status:
425 return self.disconnect_mpd()
427 self._update_current_song()
429 if self._status['songid'] != old_status['songid']:
430 self.song_changed.emit(self._status['songid'])
432 if self._status['time'] != old_status['time']:
433 self.time_changed.emit(self._status['time'])
435 if self._status['state'] != old_status['state']:
436 self.state_changed.emit(self._status['state'])
438 if self._status['volume'] != old_status['volume']:
439 self.volume_changed.emit( int(self._status['volume']))
441 if self._status['repeat'] != old_status['repeat']:
442 self.repeat_changed.emit(bool(self._status['repeat']))
444 if self._status['random'] != old_status['random']:
445 self.random_changed.emit(bool(self._status['random']))
447 if self._status['single'] != old_status['single']:
448 self.single_changed.emit(bool(self._status['single']))
450 if self._status['consume'] != old_status['consume']:
451 self.consume_changed.emit(bool(self._status['consume']))
453 if self._status['playlist'] != old_status['playlist']:
454 self.playlist_changed.emit()