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