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
26 class MPClient(QtCore
.QObject
):
27 """This class offers another layer above pympd, with usefull events."""
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,
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()
48 QtCore
.QObject
.__init
__(self
)
50 self
._cur
_playlist
= []
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...')
61 self
.logger
.warning('Attempted to connect when already connected.')
65 self
._client
= mpd
.MPDClient()
66 self
._client
.connect(host
, port
)
67 except socket
.error
, e
:
68 self
.logger
.error('Socket error: %s.'%e)
73 self
.password(password
)
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()
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)
92 def disconnect_mpd(self
):
93 """Disconnect from MPD."""
94 self
.logger
.info('Disconnecting from MPD...')
98 self
._client
.disconnect()
99 except (mpd
.ConnectionError
, socket
.error
):
103 self
.logger
.warning('Attempted to disconnect when not connected.')
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
114 self
._cur
_playlist
= []
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'):
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
135 """Get current MPD status."""
138 """Returns the current playlist."""
139 return self
._cur
_playlist
141 """Returns current library."""
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'):
156 return self
._client
.update()
157 self
._client
.command_list_ok_begin()
159 self
._client
.update(path
)
160 self
._client
.command_list_end()
163 """Returns an array of configured MPD audio outputs."""
165 return self
._retrieve
(self
._client
.outputs
)
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'):
173 self
._client
.enableoutput(output_id
)
175 self
._client
.disableoutput(output_id
)
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'):
185 volume
= min(100, max(0, volume
))
186 self
._client
.setvol(volume
)
188 def urlhandlers(self
):
189 """Returns an array of available url handlers."""
193 return self
._client
.urlhandlers()
195 """Returns a list of supported tags."""
196 if not self
._check
_command
_ok
('tagtypes'):
199 return self
._retrieve
(self
._client
.tagtypes
)
201 """List all currently available MPD commands."""
202 return self
._commands
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'):
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'):
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'):
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'):
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'):
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'):
252 self
._client
.playid(id)
254 self
._client
.playid()
257 self
.logger
.info('Pausing playback.')
258 if not self
._check
_command
_ok
('pause'):
260 self
._client
.pause(1)
262 """Resume playing."""
263 self
.logger
.info('Resuming playback.')
264 if not self
._check
_command
_ok
('pause'):
266 self
._client
.pause(0)
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'):
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'):
278 self
._client
.previous()
281 self
.logger
.info('Stopping playback.')
282 if not self
._check
_command
_ok
('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'):
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'):
297 self
._client
.command_list_ok_begin()
299 self
.logger
.info('Deleting id %d from playlist.'%id)
300 self
._client
.deleteid(id)
301 self
._client
.command_list_end()
303 """Clear current playlist."""
304 self
.logger
.info('Clearing playlist.')
305 if not self
._check
_command
_ok
('clear'):
308 def add(self
, paths
):
309 """Add all files in paths to the current playlist."""
310 if not self
._check
_command
_ok
('addid'):
313 self
._client
.command_list_ok_begin()
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
:
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'):
329 self
._client
.command_list_ok_begin()
332 self
._client
.moveid(id, target
+ i
)
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()
342 self
.logger
.error('Connection to MPD broken.')
343 self
._retr
_mutex
.unlock()
344 self
.disconnect_mpd()
347 self
._retr
_mutex
.unlock()
349 def _update_lib(self
):
350 """Update the cached library."""
351 self
._cur
_lib
= self
._array
_to
_song
_array
(self
._retrieve
(self
._client
.listallinfo
))
353 for song
in self
._cur
_lib
:
354 song
._data
['id'] = id
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
)
368 self
._cur
_song
= None
370 self
._cur
_song
= Song(song
)
371 def _update_status(self
):
372 """Get current status"""
375 ret
= self
._retrieve
(self
._client
.status
)
379 ret
['repeat'] = int(ret
['repeat'])
380 ret
['random'] = int(ret
['random'])
381 ret
['single'] = int(ret
['single'])
382 ret
['consume'] = int(ret
['consume'])
384 cur
, len = ret
['time'].split(':')
385 ret
['length'] = int(len)
386 ret
['time'] = int(cur
)
391 if not 'songid' in ret
:
395 def _check_command_ok(self
, cmd
):
397 return self
.logger
.error('Not connected.')
398 if not cmd
in self
._commands
:
399 return self
.logger
.error('Command %s not accessible'%cmd
)
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
411 self
.emit(QtCore
.SIGNAL('db_updated'))
415 old_status
= self
._status
416 self
._status
= self
._update
_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'))