Lyrics: support for loading stored lyrics.
[nephilim.git] / nephilim / mpclient.py
blobd3e0320244e3a6ee9f37494bca31b654c7d0a5cc
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 _logger = logging.getLogger('mpclient')
33 _status = {'volume' : 0, 'repeat' : 0, 'random' : 0,
34 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
35 'time' : 0, 'length' : 0, 'xfade' : 0,
36 'updatings_db' : 0,'state' : 'stop', 'single' : 0,
37 'consume' : 0}
38 _commands = None
40 _timer_id = None
42 _retr_mutex = QtCore.QMutex()
44 def __init__(self):
45 QtCore.QObject.__init__(self)
46 self._cur_lib = []
47 self._cur_playlist = []
48 self._commands = []
49 self._status = dict(MPClient._status)
51 def connect_mpd(self, host, port, password = None):
52 """Connect to MPD@host:port, optionally using password.
53 Returns True at success, False otherwise."""
55 self._logger.info('Connecting to MPD...')
56 if self._client:
57 self._logger.warning('Attempted to connect when already connected.')
58 return True
60 try:
61 self._client = mpd.MPDClient()
62 self._client.connect(host, port)
63 except socket.error, e:
64 self._logger.error('Socket error: %s.'%e)
65 self.disconnect_mpd()
66 return False
68 if password:
69 self.password(password)
70 else:
71 self._commands = self._retrieve(self._client.commands)
73 if not self._check_command_ok('listallinfo'):
74 self._logger.error('Don\'t have MPD read permission, diconnecting.')
75 return self.disconnect_mpd()
77 self._update_lib()
78 self._update_playlist()
79 self._update_current_song()
81 self.emit(QtCore.SIGNAL('connected'))
82 self.timerEvent(None)
83 self._timer_id = self.startTimer(500)
84 self._logger.info('Successfully connected to MPD.')
85 return True
86 def disconnect_mpd(self):
87 """Disconnect from MPD."""
88 self._logger.info('Disconnecting from MPD...')
89 if self._client:
90 try:
91 self._client.close()
92 self._client.disconnect()
93 except (mpd.ConnectionError, socket.error):
94 pass
95 self._client = None
96 else:
97 self._logger.warning('Attempted to disconnect when not connected.')
99 if self._timer_id:
100 self.killTimer(self._timer_id)
101 self._timer_id = None
102 self._status = dict(MPClient._status)
103 self._cur_song = None
104 self._cur_lib = []
105 self._cur_playlist = []
106 self._commands = []
107 self.emit(QtCore.SIGNAL('disconnected'))
108 def password(self, password):
109 """Use the password to authenticate with MPD."""
110 if not self._check_command_ok('password'):
111 return
112 try:
113 self._client.password(password)
114 self._logger.info('Successfully authenticated')
115 self._commands = self._retrieve(self._client.commands)
116 except mpd.CommandError:
117 self._logger.error('Incorrect MPD password.')
118 def is_connected(self):
119 """Returns True if connected to MPD, False otherwise."""
120 return self._client != None
122 def status(self):
123 """Get current MPD status."""
124 return self._status
125 def playlist(self):
126 """Returns the current playlist."""
127 return self._cur_playlist
128 def library(self):
129 """Returns current library."""
130 return self._cur_lib
131 def current_song(self):
132 """Returns the current playing song."""
133 return self._cur_song
134 def is_playing(self):
135 """Returns True if MPD is playing, False otherwise."""
136 return self._status['state'] == 'play'
138 def update_db(self, paths = None):
139 """Starts MPD database update."""
140 if not self._check_command_ok('update'):
141 return
142 if not paths:
143 return self._client.update()
144 self._client.command_list_ok_begin()
145 for path in paths:
146 self._client.update(path)
147 self._client.command_list_end()
149 def outputs(self):
150 """Returns an array of configured MPD audio outputs."""
151 if self._client:
152 return self._retrieve(self._client.outputs)
153 else:
154 return []
155 def set_output(self, output_id, state):
156 """Set audio output output_id to state (0/1)."""
157 if not self._check_command_ok('enableoutput'):
158 return
159 if state:
160 self._client.enableoutput(output_id)
161 else:
162 self._client.disableoutput(output_id)
164 def volume(self):
165 """Get current volume."""
166 return int(self._status['volume'])
167 def set_volume(self, volume):
168 """Set volume to volume."""
169 if not self._client:
170 return self._logger.error('Not connected.')
171 try:
172 volume = min(100, max(0, volume))
173 self._client.setvol(volume)
174 except mpd.CommandError, e:
175 self._logger.error('Can\'t set volume: %s.' %('don\'t have control permissions' if 'permission' in str(e)
176 else 'unknown error'))
178 def urlhandlers(self):
179 """Returns an array of available url handlers."""
180 if not self._client:
181 return []
182 else:
183 return self._client.urlhandlers()
184 def tagtypes(self):
185 """Returns a list of supported tags."""
186 if not self._check_command_ok('tagtypes'):
187 return []
189 return self._retrieve(self._client.tagtypes)
190 def commands(self):
191 """List all currently available MPD commands."""
192 return self._commands
194 def repeat(self, val):
195 """Set repeat playlist to val (True/False)."""
196 if not self._check_command_ok('repeat'):
197 return
198 if isinstance(val, bool):
199 val = 1 if val else 0
200 self._client.repeat(val)
201 def random(self, val):
202 """Set random playback to val (True, False)."""
203 if not self._check_command_ok('random'):
204 return
205 if isinstance(val, bool):
206 val = 1 if val else 0
207 self._client.random(val)
208 def crossfade(self, time):
209 """Set crossfading between songs."""
210 if not self._check_command_ok('crossfade'):
211 return
212 self._client.crossfade(time)
213 def single(self, val):
214 """Set single playback to val (True, False)"""
215 if not self._check_command_ok('single'):
216 return
217 if isinstance(val, bool):
218 val = 1 if val else 0
219 self._client.single(val)
220 def consume(self, val):
221 """Set consume mode to val (True, False)"""
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 if not self._check_command_ok('play'):
231 return
232 if id:
233 self._client.playid(id)
234 else:
235 self._client.playid()
236 def pause(self):
237 """Pause playing."""
238 if not self._check_command_ok('pause'):
239 return
240 self._client.pause(1)
241 def resume(self):
242 """Resume playing."""
243 if not self._check_command_ok('pause'):
244 return
245 self._client.pause(0)
246 def next(self):
247 """Move on to the next song in the playlist."""
248 if not self._check_command_ok('next'):
249 return
250 self._client.next()
251 def previous(self):
252 """Move back to the previous song in the playlist."""
253 if not self._check_command_ok('previous'):
254 return
255 self._client.previous()
256 def stop(self):
257 """Stop playing."""
258 if not self._check_command_ok('stop'):
259 return
260 self._client.stop()
261 def seek(self, time):
262 """Seek to time (in seconds)."""
263 if not self._check_command_ok('seekid'):
264 return
265 if self._status['songid'] > 0:
266 self._client.seekid(self._status['songid'], time)
268 def delete(self, list):
269 """Remove all song IDs in list from the playlist."""
270 if not self._check_command_ok('deleteid'):
271 return
272 self._client.command_list_ok_begin()
273 for id in list:
274 self._client.deleteid(id)
275 self._client.command_list_end()
276 def clear(self):
277 """Clear current playlist."""
278 if not self._check_command_ok('clear'):
279 return
280 self._client.clear()
281 def add(self, paths):
282 """Add all files in paths to the current playlist."""
283 if not self._check_command_ok('addid'):
284 return
285 self._client.command_list_ok_begin()
286 for path in paths:
287 self._client.addid(path.encode('utf-8'))
288 ret = self._client.command_list_end()
289 self._update_playlist()
290 if self._status['state'] == 'stop':
291 self.play(ret[0])
292 def move(self, source, target):
293 """Move the songs in playlist. Takes a list of source ids and one target position."""
294 if not self._check_command_ok('moveid'):
295 return
296 self._client.command_list_ok_begin()
297 i = 0
298 for id in source:
299 self._client.moveid(id, target + i)
300 i += 1
301 self._client.command_list_end()
303 def _retrieve(self, method):
304 """Makes sure only one call is made at a time to MPD."""
305 self._retr_mutex.lock()
306 try:
307 ret = method()
308 except socket.error:
309 self._logger.error('Connection to MPD broken.')
310 self._retr_mutex.unlock()
311 self.disconnect_mpd()
312 return None
314 self._retr_mutex.unlock()
315 return ret
316 def _update_lib(self):
317 """Update the cached library."""
318 self._cur_lib = self._array_to_song_array(self._retrieve(self._client.listallinfo))
319 id = 0
320 for song in self._cur_lib:
321 song._data['id'] = id
322 id += 1
323 def _update_playlist(self):
324 """Update the cached playlist."""
325 self._cur_playlist = self._array_to_song_array(self._retrieve(self._client.playlistinfo))
326 def _array_to_song_array(self, array):
327 """Convert an array to an array of Songs."""
328 return map(lambda entry: Song(entry)
329 , filter(lambda entry: not('directory' in entry), array)
331 def _update_current_song(self):
332 """Update the current song."""
333 song = self._retrieve(self._client.currentsong)
334 if not song:
335 self._cur_song = None
336 else:
337 self._cur_song = Song(song)
338 def _update_status(self):
339 """Get current status"""
340 if not self._client:
341 return None
342 ret = self._retrieve(self._client.status)
343 if not ret:
344 return None
346 ret['repeat'] = int(ret['repeat'])
347 ret['random'] = int(ret['random'])
348 ret['single'] = int(ret['single'])
349 ret['consume'] = int(ret['consume'])
350 if 'time' in ret:
351 cur, len = ret['time'].split(':')
352 ret['length'] = int(len)
353 ret['time'] = int(cur)
354 else:
355 ret['length'] = 0
356 ret['time'] = 0
358 if not 'updatings_db' in ret:
359 ret['updatings_db'] = 0
360 if not 'songid' in ret:
361 ret['songid'] = -1
363 return ret
364 def _check_command_ok(self, cmd):
365 if not self._client:
366 return self._logger.error('Not connected.')
367 if not cmd in self._commands:
368 return self._logger.error('Command %s not accessible'%cmd)
369 return True
371 def timerEvent(self, event):
372 """Check for changes since last check."""
373 old_status = self._status
374 self._status = self._update_status()
376 if not self._status:
377 return self.disconnect_mpd()
379 self._update_current_song()
381 if self._status['songid'] != old_status['songid']:
382 self.emit(QtCore.SIGNAL('song_changed'), self._status['songid'])
384 if self._status['time'] != old_status['time']:
385 self.emit(QtCore.SIGNAL('time_changed'), self._status['time'])
387 if self._status['state'] != old_status['state']:
388 self.emit(QtCore.SIGNAL('state_changed'), self._status['state'])
390 if self._status['volume'] != old_status['volume']:
391 self.emit(QtCore.SIGNAL('volume_changed'), int(self._status['volume']))
393 if self._status['repeat'] != old_status['repeat']:
394 self.emit(QtCore.SIGNAL('repeat_changed'), bool(self._status['repeat']))
396 if self._status['random'] != old_status['random']:
397 self.emit(QtCore.SIGNAL('random_changed'), bool(self._status['random']))
399 if self._status['single'] != old_status['single']:
400 self.emit(QtCore.SIGNAL('single_changed'), bool(self._status['single']))
402 if self._status['consume'] != old_status['consume']:
403 self.emit(QtCore.SIGNAL('consume_changed'), bool(self._status['consume']))
405 if self._status['playlist'] != old_status['playlist']:
406 self._update_playlist()
407 self.emit(QtCore.SIGNAL('playlist_changed'))
409 if self._status['updatings_db'] and not old_status['updatings_db']:
410 self.emit(QtCore.SIGNAL('update_started'))
411 if not self._status['updatings_db'] and old_status['updatings_db']:
412 self._update_lib()
413 self.emit(QtCore.SIGNAL('update_finished'))