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