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