song: make keys case-insensitive
[nephilim.git] / nephilim / mpclient.py
blobae59ebf560ec0539ce4d969c470af47cf7094db3
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, SongRef, PlaylistEntryRef
26 class MPClient(QtCore.QObject):
27 """This class offers another layer above pympd, with usefull events."""
28 # public, read-only
29 logger = None
31 # these don't change while mpd is running
32 outputs = None
33 tagtypes = None
34 urlhandlers = None
35 commands = None
37 # private
38 __password = None
39 _client = None
40 _cur_song = None
41 _status = {'volume' : 0, 'repeat' : 0, 'random' : 0,
42 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
43 'time' : 0, 'length' : 0, 'xfade' : 0,
44 'state' : 'stop', 'single' : 0,
45 'consume' : 0}
47 _timer_id = None #for querying status changes
48 _db_timer_id = None #for querying db updates
49 _db_update = None #time of last db update
51 __stats = {'artists': '0', 'albums' : '0', 'songs' : '0', 'uptime' : '0',
52 'playtime' : '0', 'db_playtime' : '0', 'db_update' : '0'}
54 # SIGNALS
55 connect_changed = QtCore.pyqtSignal(bool)
56 db_updated = QtCore.pyqtSignal()
57 song_changed = QtCore.pyqtSignal(object)
58 time_changed = QtCore.pyqtSignal(int)
59 state_changed = QtCore.pyqtSignal(str)
60 volume_changed = QtCore.pyqtSignal(int)
61 repeat_changed = QtCore.pyqtSignal(bool)
62 random_changed = QtCore.pyqtSignal(bool)
63 single_changed = QtCore.pyqtSignal(bool)
64 consume_changed = QtCore.pyqtSignal(bool)
65 playlist_changed = QtCore.pyqtSignal()
68 def __init__(self):
69 QtCore.QObject.__init__(self)
70 self.logger = logging.getLogger('mpclient')
71 self.__update_static()
72 self._status = dict(MPClient._status)
74 def connect_mpd(self, host, port, password = None):
75 """Connect to MPD@host:port, optionally using password."""
76 self.logger.info('Connecting to MPD...')
77 if self._client:
78 self.logger.warning('Attempted to connect when already connected.')
79 return
81 self._client = mpd.MPDClient()
82 self._client.connect_changed.connect(lambda val:self.__finish_connect() if val else self.__finish_disconnect())
83 self._client.connect_mpd(host, port)
84 self.__password = password
86 def disconnect_mpd(self):
87 """Disconnect from MPD."""
88 self.logger.info('Disconnecting from MPD...')
89 if self._client:
90 self._client.disconnect_mpd()
92 def password(self, password):
93 """Use the password to authenticate with MPD."""
94 self.logger.info('Authenticating with MPD.')
95 if not self.__check_command_ok('password'):
96 return
97 try:
98 self._client.password(password)
99 self.logger.info('Successfully authenticated')
100 self.__update_static()
101 except mpd.CommandError:
102 self.logger.error('Incorrect MPD password.')
103 def is_connected(self):
104 """Returns True if connected to MPD, False otherwise."""
105 return self._client != None
107 def status(self):
108 """Get current MPD status."""
109 return self._status
110 def playlistinfo(self):
111 """Returns a list of songs in current playlist."""
112 self.logger.info('Listing current playlist.')
113 if not self.__check_command_ok('playlistinfo'):
114 raise StopIteration
115 for song in self._client.playlistinfo():
116 yield Song(song)
117 raise StopIteration
118 def library(self):
119 """Returns a list of all songs in library."""
120 self.logger.info('Listing library.')
121 if not self.__check_command_ok('listallinfo'):
122 raise StopIteration
123 for song in self._client.listallinfo():
124 if 'file' in song:
125 yield Song(song)
127 raise StopIteration
128 def current_song(self):
129 """Returns the current playing song."""
130 return self._cur_song
131 def is_playing(self):
132 """Returns True if MPD is playing, False otherwise."""
133 return self._status['state'] == 'play'
134 def find(self, *args):
135 if not self.__check_command_ok('find'):
136 raise StopIteration
137 for song in self._client.find(*args):
138 yield Song(song)
139 raise StopIteration
140 def findadd(self, *args):
141 """Find tracks with given tags and add them to playlist. Takes
142 a list of (tag, value)."""
143 self.logger.info('Findadd %s.'%unicode(args))
144 if not self.__check_command_ok('findadd'):
145 return
146 return self._client.findadd(*args)
147 def playlistid(self, plid):
148 """Return a song with a given playlist id."""
149 self.logger.info('Getting id %s.'%('of id %s'%(plid) if plid else ''))
150 if not self.__check_command_ok('play'):
151 return
152 ret = None
153 for it in self._client.playlistid(plid):
154 ret = Song(it)
155 return ret
157 def update_db(self, paths = None):
158 """Starts MPD database update."""
159 self.logger.info('Updating database %s'%(paths if paths else '.'))
160 if not self.__check_command_ok('update'):
161 return
162 if not paths:
163 return self._client.update()
164 self._client.command_list_ok_begin()
165 for path in paths:
166 self._client.update(path)
167 list(self._client.command_list_end())
169 def volume(self):
170 """Get current volume."""
171 return int(self._status['volume'])
172 def set_volume(self, volume):
173 """Set volume to volume."""
174 self.logger.info('Setting volume to %d.'%volume)
175 if not self.__check_command_ok('setvol'):
176 return
177 volume = min(100, max(0, volume))
178 try:
179 self._client.setvol(volume)
180 except mpd.CommandError, e:
181 self.logger.warning('Error setting volume (probably no outputs enabled): %s.'%e)
183 def stats(self):
184 """Get MPD statistics."""
185 if not self.__check_command_ok('stats'):
186 return self.__stats
187 return self._client.stats()
189 def repeat(self, val):
190 """Set repeat playlist to val (True/False)."""
191 self.logger.info('Setting repeat to %d.'%val)
192 if not self.__check_command_ok('repeat'):
193 return
194 if isinstance(val, bool):
195 val = 1 if val else 0
196 self._client.repeat(val)
197 def random(self, val):
198 """Set random playback to val (True, False)."""
199 self.logger.info('Setting random to %d.'%val)
200 if not self.__check_command_ok('random'):
201 return
202 if isinstance(val, bool):
203 val = 1 if val else 0
204 self._client.random(val)
205 def crossfade(self, time):
206 """Set crossfading between songs."""
207 self.logger.info('Setting crossfade to %d'%time)
208 if not self.__check_command_ok('crossfade'):
209 return
210 self._client.crossfade(time)
211 def single(self, val):
212 """Set single playback to val (True, False)"""
213 self.logger.info('Setting single to %d.'%val)
214 if not self.__check_command_ok('single'):
215 return
216 if isinstance(val, bool):
217 val = 1 if val else 0
218 self._client.single(val)
219 def consume(self, val):
220 """Set consume mode to val (True, False)"""
221 self.logger.info('Setting consume to %d.'%val)
222 if not self.__check_command_ok('consume'):
223 return
224 if isinstance(val, bool):
225 val = 1 if val else 0
226 self._client.consume(val)
228 def play(self, id = None):
229 """Play song with ID id or next song if id is None."""
230 self.logger.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
231 if not self.__check_command_ok('play'):
232 return
233 if id:
234 self._client.playid(id)
235 else:
236 self._client.playid()
237 def pause(self):
238 """Pause playing."""
239 self.logger.info('Pausing playback.')
240 if not self.__check_command_ok('pause'):
241 return
242 self._client.pause(1)
243 def resume(self):
244 """Resume playing."""
245 self.logger.info('Resuming playback.')
246 if not self.__check_command_ok('pause'):
247 return
248 self._client.pause(0)
249 def next(self):
250 """Move on to the next song in the playlist."""
251 self.logger.info('Skipping to next song.')
252 if not self.__check_command_ok('next'):
253 return
254 self._client.next()
255 def previous(self):
256 """Move back to the previous song in the playlist."""
257 self.logger.info('Moving to previous song.')
258 if not self.__check_command_ok('previous'):
259 return
260 self._client.previous()
261 def stop(self):
262 """Stop playing."""
263 self.logger.info('Stopping playback.')
264 if not self.__check_command_ok('stop'):
265 return
266 self._client.stop()
267 def seek(self, time):
268 """Seek to time (in seconds)."""
269 self.logger.info('Seeking to %d.'%time)
270 if not self.__check_command_ok('seekid'):
271 return
272 if self._status['songid'] > 0:
273 self._client.seekid(self._status['songid'], time)
275 def delete(self, ids):
276 """Remove all song IDs in list from the playlist."""
277 if not self.__check_command_ok('deleteid'):
278 return
279 self._client.command_list_ok_begin()
280 try:
281 for id in ids:
282 self.logger.info('Deleting id %s from playlist.'%id)
283 self._client.deleteid(id)
284 list(self._client.command_list_end())
285 except mpd.CommandError, e:
286 self.logger.error('Error deleting files: %s.'%e)
287 def clear(self):
288 """Clear current playlist."""
289 self.logger.info('Clearing playlist.')
290 if not self.__check_command_ok('clear'):
291 return
292 self._client.clear()
293 def add(self, paths, pos = -1):
294 """Add all files in paths to the current playlist."""
295 if not self.__check_command_ok('addid'):
296 return
297 ret = None
298 self._client.command_list_ok_begin()
299 for path in paths:
300 self.logger.info('Adding %s to playlist'%path)
301 if pos < 0:
302 self._client.addid(path)
303 else:
304 self._client.addid(path, pos)
305 pos += 1
306 try:
307 ret = list(self._client.command_list_end())
308 except mpd.CommandError, e:
309 self.logger.error('Error adding files: %s.'%e)
310 if self._status['state'] == 'stop' and ret:
311 self.play(ret[0])
312 def move(self, source, target):
313 """Move the songs in playlist. Takes one source id and one target position."""
314 self.logger.info('Moving %s to %s.'%(source, target))
315 if not self.__check_command_ok('moveid'):
316 return
317 self._client.moveid(source, target)
319 #### private ####
320 def __finish_connect(self):
321 if self.__password:
322 self.password(self.__password)
323 else:
324 self.__update_static()
326 if not self.__check_command_ok('listallinfo'):
327 self.logger.error('Don\'t have MPD read permission, diconnecting.')
328 return self.disconnect_mpd()
330 self.__update_current_song()
331 self._db_update = self.stats()['db_update']
333 self.connect_changed.emit(True)
334 self.logger.info('Successfully connected to MPD.')
335 self._timer_id = self.startTimer(500)
336 self._db_timer_id = self.startTimer(1000)
337 def __finish_disconnect(self):
338 self._client = None
340 if self._timer_id:
341 self.killTimer(self._timer_id)
342 self._timer_id = None
343 if self._db_timer_id:
344 self.killTimer(self._db_timer_id)
345 self._db_timer_id = None
346 self._status = dict(MPClient._status)
347 self._cur_song = None
348 self.__update_static()
349 self.connect_changed.emit(False)
350 self.logger.info('Disconnected from MPD.')
351 def __update_current_song(self):
352 """Update the current song."""
353 song = self._client.currentsong()
354 if not song:
355 self._cur_song = None
356 else:
357 self._cur_song = Song(song)
358 def _update_status(self):
359 """Get current status"""
360 if not self._client:
361 return None
362 ret = self._client.status()
363 if not ret:
364 return None
366 ret['repeat'] = int(ret['repeat'])
367 ret['random'] = int(ret['random'])
368 ret['single'] = int(ret['single'])
369 ret['consume'] = int(ret['consume'])
370 ret['volume'] = int(ret['volume'])
371 if 'time' in ret:
372 cur, len = ret['time'].split(':')
373 ret['length'] = int(len)
374 ret['time'] = int(cur)
375 else:
376 ret['length'] = 0
377 ret['time'] = 0
379 if not 'songid' in ret:
380 ret['songid'] = '-1'
382 return ret
383 def __check_command_ok(self, cmd):
384 if not self._client:
385 return self.logger.info('Not connected.')
386 if not cmd in self.commands:
387 return self.logger.error('Command %s not accessible'%cmd)
388 return True
390 def __update_static(self):
391 """Update static values, called on connect/disconnect."""
392 if self._client:
393 self.commands = list(self._client.commands())
394 else:
395 self.commands = []
397 if self.__check_command_ok('outputs'):
398 outputs = []
399 for output in self._client.outputs():
400 outputs.append(AudioOutput(self, output['outputname'], output['outputid'],
401 bool(output['outputenabled'])))
402 self.outputs = outputs
403 else:
404 self.outputs = []
406 if self.__check_command_ok('tagtypes'):
407 self.tagtypes = map(unicode.lower, self._client.tagtypes()) + ['file']
408 else:
409 self.tagtypes = []
411 if self.__check_command_ok('urlhandlers'):
412 self.urlhandlers = list(self._client.urlhandlers())
413 else:
414 self.urlhandlers = []
416 def set_output(self, output_id, state):
417 """Set audio output output_id to state (0/1). Called only by AudioOutput."""
418 if not self.__check_command_ok('enableoutput'):
419 return
420 if state:
421 self._client.enableoutput(output_id)
422 else:
423 self._client.disableoutput(output_id)
425 def timerEvent(self, event):
426 """Check for changes since last check."""
427 if event.timerId() == self._db_timer_id:
428 #timer for monitoring db changes
429 db_update = self.stats()['db_update']
430 if db_update > self._db_update:
431 self.logger.info('Database updated.')
432 self._db_update = db_update
433 self.db_updated.emit()
434 return
437 old_status = self._status
438 self._status = self._update_status()
440 if not self._status:
441 self.logger.error('Error reading status.')
442 return self.disconnect_mpd()
444 if self._status['songid'] != old_status['songid']:
445 self.__update_current_song()
446 self.song_changed.emit(PlaylistEntryRef(self, self._status['songid']))
448 if self._status['time'] != old_status['time']:
449 self.time_changed.emit(self._status['time'])
451 if self._status['state'] != old_status['state']:
452 self.state_changed.emit(self._status['state'])
454 if self._status['volume'] != old_status['volume']:
455 self.volume_changed.emit( int(self._status['volume']))
457 if self._status['repeat'] != old_status['repeat']:
458 self.repeat_changed.emit(bool(self._status['repeat']))
460 if self._status['random'] != old_status['random']:
461 self.random_changed.emit(bool(self._status['random']))
463 if self._status['single'] != old_status['single']:
464 self.single_changed.emit(bool(self._status['single']))
466 if self._status['consume'] != old_status['consume']:
467 self.consume_changed.emit(bool(self._status['consume']))
469 if self._status['playlist'] != old_status['playlist']:
470 self.playlist_changed.emit()
472 outputs = list(self._client.outputs())
473 for i in range(len(outputs)):
474 if int(outputs[i]['outputenabled']) != int(self.outputs[i].state):
475 self.outputs[i].mpd_toggle_state()
478 class AudioOutput(QtCore.QObject):
479 """This class represents an MPD audio output."""
480 # public, const
481 mpclient = None
482 name = None
483 id = None
484 state = None
486 # SIGNALS
487 state_changed = QtCore.pyqtSignal(bool)
489 #### public ####
490 def __init__(self, mpclient, name, id, state):
491 QtCore.QObject.__init__(self)
493 self.mpclient = mpclient
494 self.name = name
495 self.id = id
496 self.state = state
498 @QtCore.pyqtSlot(bool)
499 def set_state(self, state):
500 self.mpclient.set_output(self.id, state)
502 #### private ####
503 def mpd_toggle_state(self):
504 """This is called by mpclient to inform about output state change."""
505 self.state = not self.state
506 self.state_changed.emit(self.state)