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
24 from song
import Song
, SongRef
, PlaylistEntryRef
26 class MPClient(QtCore
.QObject
):
27 """This class offers another layer above pympd, with usefull events."""
29 "a list of audio outputs"
35 _status
= {'volume' : 0, 'repeat' : 0, 'random' : 0,
36 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
37 'time' : 0, 'length' : 0, 'xfade' : 0,
38 'state' : 'stop', 'single' : 0,
42 _timer_id
= None #for querying status changes
43 _db_timer_id
= None #for querying db updates
44 _db_update
= None #time of last db update
46 _retr_mutex
= QtCore
.QMutex()
51 connect_changed
= QtCore
.pyqtSignal(bool)
52 db_updated
= QtCore
.pyqtSignal()
53 song_changed
= QtCore
.pyqtSignal(object)
54 time_changed
= QtCore
.pyqtSignal(int)
55 state_changed
= QtCore
.pyqtSignal(str)
56 volume_changed
= QtCore
.pyqtSignal(int)
57 repeat_changed
= QtCore
.pyqtSignal(bool)
58 random_changed
= QtCore
.pyqtSignal(bool)
59 single_changed
= QtCore
.pyqtSignal(bool)
60 consume_changed
= QtCore
.pyqtSignal(bool)
61 playlist_changed
= QtCore
.pyqtSignal()
63 class Output(QtCore
.QObject
):
64 """This class represents an MPD audio output."""
72 state_changed
= QtCore
.pyqtSignal(bool)
75 def __init__(self
, mpclient
, name
, id, state
):
76 QtCore
.QObject
.__init
__(self
, mpclient
)
78 self
.mpclient
= mpclient
83 @QtCore.pyqtSlot(bool)
84 def set_state(self
, state
):
85 self
.mpclient
.set_output(self
.id, state
)
88 def mpd_toggle_state(self
):
89 """This is called by mpclient to inform about output state change."""
90 self
.state
= not self
.state
91 self
.state_changed
.emit(self
.state
)
94 QtCore
.QObject
.__init
__(self
)
98 self
._status
= dict(MPClient
._status
)
99 self
.logger
= logging
.getLogger('mpclient')
101 def connect_mpd(self
, host
, port
, password
= None):
102 """Connect to MPD@host:port, optionally using password.
103 Returns True at success, False otherwise."""
105 self
.logger
.info('Connecting to MPD...')
107 self
.logger
.warning('Attempted to connect when already connected.')
111 self
._client
= mpd
.MPDClient()
112 self
._client
.connect(host
, port
)
113 except socket
.error
, e
:
114 self
.logger
.error('Socket error: %s.'%e)
115 self
.disconnect_mpd()
119 self
.password(password
)
121 self
._commands
= self
._retrieve
(self
._client
.commands
)
123 if not self
.__check
_command
_ok
('listallinfo'):
124 self
.logger
.error('Don\'t have MPD read permission, diconnecting.')
125 return self
.disconnect_mpd()
127 self
.__update
_current
_song
()
128 self
.__update
_outputs
()
129 self
._db
_update
= self
.stats()['db_update']
131 self
.emit(QtCore
.SIGNAL('connected')) #should be removed
132 self
.connect_changed
.emit(True)
133 self
.logger
.info('Successfully connected to MPD.')
134 self
._timer
_id
= self
.startTimer(500)
135 self
._db
_timer
_id
= self
.startTimer(10000)
137 def disconnect_mpd(self
):
138 """Disconnect from MPD."""
139 self
.logger
.info('Disconnecting from MPD...')
143 self
._client
.disconnect()
144 except (mpd
.ConnectionError
, socket
.error
):
148 self
.logger
.warning('Attempted to disconnect when not connected.')
151 self
.killTimer(self
._timer
_id
)
152 self
._timer
_id
= None
153 if self
._db
_timer
_id
:
154 self
.killTimer(self
._db
_timer
_id
)
155 self
._db
_timer
_id
= None
156 self
._status
= dict(MPClient
._status
)
157 self
._cur
_song
= None
160 self
.emit(QtCore
.SIGNAL('disconnected')) #should be removed
161 self
.connect_changed
.emit(False)
162 self
.logger
.info('Disconnected from MPD.')
163 def password(self
, password
):
164 """Use the password to authenticate with MPD."""
165 self
.logger
.info('Authenticating with MPD.')
166 if not self
.__check
_command
_ok
('password'):
169 self
._client
.password(password
)
170 self
.logger
.info('Successfully authenticated')
171 self
._commands
= self
._retrieve
(self
._client
.commands
)
172 except mpd
.CommandError
:
173 self
.logger
.error('Incorrect MPD password.')
174 def is_connected(self
):
175 """Returns True if connected to MPD, False otherwise."""
176 return self
._client
!= None
179 """Get current MPD status."""
181 def playlistinfo(self
):
182 """Returns a list of songs in current playlist."""
183 self
.logger
.info('Listing current playlist.')
184 if not self
.__check
_command
_ok
('playlistinfo'):
186 return self
._array
_to
_song
_array
(self
._retrieve
(self
._client
.playlistinfo
))
188 """Returns a list of all songs in library."""
189 self
.logger
.info('Listing library.')
190 if not self
.__check
_command
_ok
('listallinfo'):
192 return self
._array
_to
_song
_array
(self
._retrieve
(self
._client
.listallinfo
))
193 def current_song(self
):
194 """Returns the current playing song."""
195 return self
._cur
_song
196 def is_playing(self
):
197 """Returns True if MPD is playing, False otherwise."""
198 return self
._status
['state'] == 'play'
199 def find(self
, *args
):
200 if not self
.__check
_command
_ok
('find'):
202 return self
._array
_to
_song
_array
(self
._client
.find(*args
))
203 def findadd(self
, *args
):
204 if not self
.__check
_command
_ok
('findadd'):
206 return self
._client
.findadd(*args
)
208 def update_db(self
, paths
= None):
209 """Starts MPD database update."""
210 self
.logger
.info('Updating database %s'%(paths
if paths
else '.'))
211 if not self
.__check
_command
_ok
('update'):
214 return self
._client
.update()
215 self
._client
.command_list_ok_begin()
217 self
._client
.update(path
)
218 self
._client
.command_list_end()
222 """Get current volume."""
223 return int(self
._status
['volume'])
224 def set_volume(self
, volume
):
225 """Set volume to volume."""
226 self
.logger
.info('Setting volume to %d.'%volume
)
227 if not self
.__check
_command
_ok
('setvol'):
229 volume
= min(100, max(0, volume
))
231 self
._client
.setvol(volume
)
232 except mpd
.CommandError
, e
:
233 self
.logger
.warning('Error setting volume (probably no outputs enabled): %s.'%e)
235 def urlhandlers(self
):
236 """Returns an array of available url handlers."""
240 return self
._client
.urlhandlers()
242 """Returns a list of supported tags."""
243 if not self
.__check
_command
_ok
('tagtypes'):
246 return map(str.lower
, self
._retrieve
(self
._client
.tagtypes
) + ['file'])
248 """List all currently available MPD commands."""
249 return self
._commands
251 """Get MPD statistics."""
252 return self
._retrieve
(self
._client
.stats
)
254 def repeat(self
, val
):
255 """Set repeat playlist to val (True/False)."""
256 self
.logger
.info('Setting repeat to %d.'%val
)
257 if not self
.__check
_command
_ok
('repeat'):
259 if isinstance(val
, bool):
260 val
= 1 if val
else 0
261 self
._client
.repeat(val
)
262 def random(self
, val
):
263 """Set random playback to val (True, False)."""
264 self
.logger
.info('Setting random to %d.'%val
)
265 if not self
.__check
_command
_ok
('random'):
267 if isinstance(val
, bool):
268 val
= 1 if val
else 0
269 self
._client
.random(val
)
270 def crossfade(self
, time
):
271 """Set crossfading between songs."""
272 self
.logger
.info('Setting crossfade to %d'%time
)
273 if not self
.__check
_command
_ok
('crossfade'):
275 self
._client
.crossfade(time
)
276 def single(self
, val
):
277 """Set single playback to val (True, False)"""
278 self
.logger
.info('Setting single to %d.'%val
)
279 if not self
.__check
_command
_ok
('single'):
281 if isinstance(val
, bool):
282 val
= 1 if val
else 0
283 self
._client
.single(val
)
284 def consume(self
, val
):
285 """Set consume mode to val (True, False)"""
286 self
.logger
.info('Setting consume to %d.'%val
)
287 if not self
.__check
_command
_ok
('consume'):
289 if isinstance(val
, bool):
290 val
= 1 if val
else 0
291 self
._client
.consume(val
)
293 def play(self
, id = None):
294 """Play song with ID id or next song if id is None."""
295 self
.logger
.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
296 if not self
.__check
_command
_ok
('play'):
299 self
._client
.playid(id)
301 self
._client
.playid()
304 self
.logger
.info('Pausing playback.')
305 if not self
.__check
_command
_ok
('pause'):
307 self
._client
.pause(1)
309 """Resume playing."""
310 self
.logger
.info('Resuming playback.')
311 if not self
.__check
_command
_ok
('pause'):
313 self
._client
.pause(0)
315 """Move on to the next song in the playlist."""
316 self
.logger
.info('Skipping to next song.')
317 if not self
.__check
_command
_ok
('next'):
321 """Move back to the previous song in the playlist."""
322 self
.logger
.info('Moving to previous song.')
323 if not self
.__check
_command
_ok
('previous'):
325 self
._client
.previous()
328 self
.logger
.info('Stopping playback.')
329 if not self
.__check
_command
_ok
('stop'):
332 def seek(self
, time
):
333 """Seek to time (in seconds)."""
334 self
.logger
.info('Seeking to %d.'%time
)
335 if not self
.__check
_command
_ok
('seekid'):
337 if self
._status
['songid'] > 0:
338 self
._client
.seekid(self
._status
['songid'], time
)
340 def delete(self
, list):
341 """Remove all song IDs in list from the playlist."""
342 if not self
.__check
_command
_ok
('deleteid'):
344 self
._client
.command_list_ok_begin()
347 self
.logger
.info('Deleting id %s from playlist.'%id)
348 self
._client
.deleteid(id)
349 self
._client
.command_list_end()
350 except mpd
.CommandError
, e
:
351 self
.logger
.error('Error deleting files: %s.'%e)
353 """Clear current playlist."""
354 self
.logger
.info('Clearing playlist.')
355 if not self
.__check
_command
_ok
('clear'):
358 def add(self
, paths
):
359 """Add all files in paths to the current playlist."""
360 if not self
.__check
_command
_ok
('addid'):
363 self
._client
.command_list_ok_begin()
366 self
.logger
.info('Adding %s to playlist'%path
)
367 self
._client
.addid(path
.encode('utf-8'))
368 ret
= self
._client
.command_list_end()
369 except mpd
.CommandError
, e
:
370 self
.logger
.error('Error adding files: %s.'%e)
371 if self
._status
['state'] == 'stop' and ret
:
373 def move(self
, source
, target
):
374 """Move the songs in playlist. Takes a list of source ids and one target position."""
375 self
.logger
.info('Moving %d to %d.'%(source
, target
))
376 if not self
.__check
_command
_ok
('moveid'):
378 self
._client
.command_list_ok_begin()
381 self
._client
.moveid(id, target
+ i
)
383 self
._client
.command_list_end()
386 def _retrieve(self
, method
):
387 """Makes sure only one call is made at a time to MPD."""
388 self
._retr
_mutex
.lock()
392 self
.logger
.error('Connection to MPD broken.')
393 self
._retr
_mutex
.unlock()
394 self
.disconnect_mpd()
397 self
._retr
_mutex
.unlock()
399 def _array_to_song_array(self
, array
):
400 """Convert an array to an array of Songs."""
401 return map(lambda entry
: Song(entry
)
402 , filter(lambda entry
: not('directory' in entry
), array
)
404 def __update_current_song(self
):
405 """Update the current song."""
406 song
= self
._retrieve
(self
._client
.currentsong
)
408 self
._cur
_song
= None
410 self
._cur
_song
= Song(song
)
411 def _update_status(self
):
412 """Get current status"""
415 ret
= self
._retrieve
(self
._client
.status
)
419 ret
['repeat'] = int(ret
['repeat'])
420 ret
['random'] = int(ret
['random'])
421 ret
['single'] = int(ret
['single'])
422 ret
['consume'] = int(ret
['consume'])
424 cur
, len = ret
['time'].split(':')
425 ret
['length'] = int(len)
426 ret
['time'] = int(cur
)
431 if not 'songid' in ret
:
435 def __check_command_ok(self
, cmd
):
437 return self
.logger
.info('Not connected.')
438 if not cmd
in self
._commands
:
439 return self
.logger
.error('Command %s not accessible'%cmd
)
442 def __update_outputs(self
):
443 """Update the list of MPD audio outputs."""
444 if self
.__check
_command
_ok
('outputs'):
446 for output
in self
._retrieve
(self
._client
.outputs
):
447 outputs
.append(MPClient
.Output(self
, output
['outputname'], output
['outputid'],
448 bool(output
['outputenabled'])))
449 self
.outputs
= outputs
452 def set_output(self
, output_id
, state
):
453 """Set audio output output_id to state (0/1)."""
454 if not self
.__check
_command
_ok
('enableoutput'):
457 self
._client
.enableoutput(output_id
)
459 self
._client
.disableoutput(output_id
)
461 def timerEvent(self
, event
):
462 """Check for changes since last check."""
463 if event
.timerId() == self
._db
_timer
_id
:
464 #timer for monitoring db changes
465 db_update
= self
.stats()['db_update']
466 if db_update
> self
._db
_update
:
467 self
.logger
.info('Database updated.')
468 self
._db
_update
= db_update
469 self
.db_updated
.emit()
473 old_status
= self
._status
474 self
._status
= self
._update
_status
()
477 return self
.disconnect_mpd()
479 self
.__update
_current
_song
()
481 if self
._status
['songid'] != old_status
['songid']:
482 self
.song_changed
.emit(PlaylistEntryRef(self
, self
._status
['songid']))
484 if self
._status
['time'] != old_status
['time']:
485 self
.time_changed
.emit(self
._status
['time'])
487 if self
._status
['state'] != old_status
['state']:
488 self
.state_changed
.emit(self
._status
['state'])
490 if self
._status
['volume'] != old_status
['volume']:
491 self
.volume_changed
.emit( int(self
._status
['volume']))
493 if self
._status
['repeat'] != old_status
['repeat']:
494 self
.repeat_changed
.emit(bool(self
._status
['repeat']))
496 if self
._status
['random'] != old_status
['random']:
497 self
.random_changed
.emit(bool(self
._status
['random']))
499 if self
._status
['single'] != old_status
['single']:
500 self
.single_changed
.emit(bool(self
._status
['single']))
502 if self
._status
['consume'] != old_status
['consume']:
503 self
.consume_changed
.emit(bool(self
._status
['consume']))
505 if self
._status
['playlist'] != old_status
['playlist']:
506 self
.playlist_changed
.emit()
508 outputs
= self
._retrieve
(self
._client
.outputs
)
509 for i
in range(len(outputs
)):
510 if int(outputs
[i
]['outputenabled']) != int(self
.outputs
[i
].state
):
511 self
.outputs
[i
].mpd_toggle_state()