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