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"
36 _status
= {'volume' : 0, 'repeat' : 0, 'random' : 0,
37 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
38 'time' : 0, 'length' : 0, 'xfade' : 0,
39 'state' : 'stop', 'single' : 0,
43 _timer_id
= None #for querying status changes
44 _db_timer_id
= None #for querying db updates
45 _db_update
= None #time of last db update
50 connect_changed
= QtCore
.pyqtSignal(bool)
51 db_updated
= QtCore
.pyqtSignal()
52 song_changed
= QtCore
.pyqtSignal(object)
53 time_changed
= QtCore
.pyqtSignal(int)
54 state_changed
= QtCore
.pyqtSignal(str)
55 volume_changed
= QtCore
.pyqtSignal(int)
56 repeat_changed
= QtCore
.pyqtSignal(bool)
57 random_changed
= QtCore
.pyqtSignal(bool)
58 single_changed
= QtCore
.pyqtSignal(bool)
59 consume_changed
= QtCore
.pyqtSignal(bool)
60 playlist_changed
= QtCore
.pyqtSignal()
62 class Output(QtCore
.QObject
):
63 """This class represents an MPD audio output."""
71 state_changed
= QtCore
.pyqtSignal(bool)
74 def __init__(self
, mpclient
, name
, id, state
):
75 QtCore
.QObject
.__init
__(self
)
77 self
.mpclient
= mpclient
82 @QtCore.pyqtSlot(bool)
83 def set_state(self
, state
):
84 self
.mpclient
.set_output(self
.id, state
)
87 def mpd_toggle_state(self
):
88 """This is called by mpclient to inform about output state change."""
89 self
.state
= not self
.state
90 self
.state_changed
.emit(self
.state
)
93 QtCore
.QObject
.__init
__(self
)
97 self
._status
= dict(MPClient
._status
)
98 self
.logger
= logging
.getLogger('mpclient')
100 def connect_mpd(self
, host
, port
, password
= None):
101 """Connect to MPD@host:port, optionally using password."""
102 self
.logger
.info('Connecting to MPD...')
104 self
.logger
.warning('Attempted to connect when already connected.')
107 self
._client
= mpd
.MPDClient()
108 self
._client
.connect_changed
.connect(lambda val
:self
.__finish
_connect
() if val
else self
.__finish
_disconnect
())
109 self
._client
.connect_mpd(host
, port
)
110 self
.__password
= password
112 def disconnect_mpd(self
):
113 """Disconnect from MPD."""
114 self
.logger
.info('Disconnecting from MPD...')
116 self
.logger
.warning('Attempted to disconnect when not connected.')
117 self
._client
.disconnect_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
= list(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."""
137 def playlistinfo(self
):
138 """Returns a list of songs in current playlist."""
139 self
.logger
.info('Listing current playlist.')
140 if not self
.__check
_command
_ok
('playlistinfo'):
142 for song
in self
._client
.playlistinfo():
146 """Returns a list of all songs in library."""
147 self
.logger
.info('Listing library.')
148 if not self
.__check
_command
_ok
('listallinfo'):
150 for song
in self
._client
.listallinfo():
155 def current_song(self
):
156 """Returns the current playing song."""
157 return self
._cur
_song
158 def is_playing(self
):
159 """Returns True if MPD is playing, False otherwise."""
160 return self
._status
['state'] == 'play'
161 def find(self
, *args
):
162 if not self
.__check
_command
_ok
('find'):
164 for song
in self
._client
.find(*args
):
167 def findadd(self
, *args
):
168 if not self
.__check
_command
_ok
('findadd'):
170 return self
._client
.findadd(*args
)
172 def update_db(self
, paths
= None):
173 """Starts MPD database update."""
174 self
.logger
.info('Updating database %s'%(paths
if paths
else '.'))
175 if not self
.__check
_command
_ok
('update'):
178 return self
._client
.update()
179 self
._client
.command_list_ok_begin()
181 self
._client
.update(path
)
182 self
._client
.command_list_end()
186 """Get current volume."""
187 return int(self
._status
['volume'])
188 def set_volume(self
, volume
):
189 """Set volume to volume."""
190 self
.logger
.info('Setting volume to %d.'%volume
)
191 if not self
.__check
_command
_ok
('setvol'):
193 volume
= min(100, max(0, volume
))
195 self
._client
.setvol(volume
)
196 except mpd
.CommandError
, e
:
197 self
.logger
.warning('Error setting volume (probably no outputs enabled): %s.'%e)
199 def urlhandlers(self
):
200 """Returns an array of available url handlers."""
204 return self
._client
.urlhandlers()
206 """Returns a list of supported tags."""
207 if not self
.__check
_command
_ok
('tagtypes'):
210 return map(str.lower
, list(self
._client
.tagtypes()) + ['file'])
212 """List all currently available MPD commands."""
213 return self
._commands
215 """Get MPD statistics."""
216 return self
._client
.stats()
218 def repeat(self
, val
):
219 """Set repeat playlist to val (True/False)."""
220 self
.logger
.info('Setting repeat to %d.'%val
)
221 if not self
.__check
_command
_ok
('repeat'):
223 if isinstance(val
, bool):
224 val
= 1 if val
else 0
225 self
._client
.repeat(val
)
226 def random(self
, val
):
227 """Set random playback to val (True, False)."""
228 self
.logger
.info('Setting random to %d.'%val
)
229 if not self
.__check
_command
_ok
('random'):
231 if isinstance(val
, bool):
232 val
= 1 if val
else 0
233 self
._client
.random(val
)
234 def crossfade(self
, time
):
235 """Set crossfading between songs."""
236 self
.logger
.info('Setting crossfade to %d'%time
)
237 if not self
.__check
_command
_ok
('crossfade'):
239 self
._client
.crossfade(time
)
240 def single(self
, val
):
241 """Set single playback to val (True, False)"""
242 self
.logger
.info('Setting single to %d.'%val
)
243 if not self
.__check
_command
_ok
('single'):
245 if isinstance(val
, bool):
246 val
= 1 if val
else 0
247 self
._client
.single(val
)
248 def consume(self
, val
):
249 """Set consume mode to val (True, False)"""
250 self
.logger
.info('Setting consume to %d.'%val
)
251 if not self
.__check
_command
_ok
('consume'):
253 if isinstance(val
, bool):
254 val
= 1 if val
else 0
255 self
._client
.consume(val
)
257 def play(self
, id = None):
258 """Play song with ID id or next song if id is None."""
259 self
.logger
.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
260 if not self
.__check
_command
_ok
('play'):
263 self
._client
.playid(id)
265 self
._client
.playid()
268 self
.logger
.info('Pausing playback.')
269 if not self
.__check
_command
_ok
('pause'):
271 self
._client
.pause(1)
273 """Resume playing."""
274 self
.logger
.info('Resuming playback.')
275 if not self
.__check
_command
_ok
('pause'):
277 self
._client
.pause(0)
279 """Move on to the next song in the playlist."""
280 self
.logger
.info('Skipping to next song.')
281 if not self
.__check
_command
_ok
('next'):
285 """Move back to the previous song in the playlist."""
286 self
.logger
.info('Moving to previous song.')
287 if not self
.__check
_command
_ok
('previous'):
289 self
._client
.previous()
292 self
.logger
.info('Stopping playback.')
293 if not self
.__check
_command
_ok
('stop'):
296 def seek(self
, time
):
297 """Seek to time (in seconds)."""
298 self
.logger
.info('Seeking to %d.'%time
)
299 if not self
.__check
_command
_ok
('seekid'):
301 if self
._status
['songid'] > 0:
302 self
._client
.seekid(self
._status
['songid'], time
)
304 def delete(self
, list):
305 """Remove all song IDs in list from the playlist."""
306 if not self
.__check
_command
_ok
('deleteid'):
308 self
._client
.command_list_ok_begin()
311 self
.logger
.info('Deleting id %s from playlist.'%id)
312 self
._client
.deleteid(id)
313 self
._client
.command_list_end()
314 except mpd
.CommandError
, e
:
315 self
.logger
.error('Error deleting files: %s.'%e)
317 """Clear current playlist."""
318 self
.logger
.info('Clearing playlist.')
319 if not self
.__check
_command
_ok
('clear'):
322 def add(self
, paths
):
323 """Add all files in paths to the current playlist."""
324 if not self
.__check
_command
_ok
('addid'):
327 self
._client
.command_list_ok_begin()
330 self
.logger
.info('Adding %s to playlist'%path
)
331 self
._client
.addid(path
.encode('utf-8'))
332 ret
= list(self
._client
.command_list_end())
333 except mpd
.CommandError
, e
:
334 self
.logger
.error('Error adding files: %s.'%e)
335 if self
._status
['state'] == 'stop' and ret
:
337 def move(self
, source
, target
):
338 """Move the songs in playlist. Takes a list of source ids and one target position."""
339 self
.logger
.info('Moving %d to %d.'%(source
, target
))
340 if not self
.__check
_command
_ok
('moveid'):
342 self
._client
.command_list_ok_begin()
345 self
._client
.moveid(id, target
+ i
)
347 self
._client
.command_list_end()
350 def __finish_connect(self
):
352 self
.password(self
.__password
)
354 self
._commands
= list(self
._client
.commands())
356 if not self
.__check
_command
_ok
('listallinfo'):
357 self
.logger
.error('Don\'t have MPD read permission, diconnecting.')
358 return self
.disconnect_mpd()
360 self
.__update
_current
_song
()
361 self
.__update
_outputs
()
362 self
._db
_update
= self
.stats()['db_update']
364 self
.connect_changed
.emit(True)
365 self
.logger
.info('Successfully connected to MPD.')
366 self
._timer
_id
= self
.startTimer(500)
367 self
._db
_timer
_id
= self
.startTimer(1000)
368 def __finish_disconnect(self
):
372 self
.killTimer(self
._timer
_id
)
373 self
._timer
_id
= None
374 if self
._db
_timer
_id
:
375 self
.killTimer(self
._db
_timer
_id
)
376 self
._db
_timer
_id
= None
377 self
._status
= dict(MPClient
._status
)
378 self
._cur
_song
= None
381 self
.connect_changed
.emit(False)
382 self
.logger
.info('Disconnected from MPD.')
383 def __update_current_song(self
):
384 """Update the current song."""
385 song
= self
._client
.currentsong()
387 self
._cur
_song
= None
389 self
._cur
_song
= Song(song
)
390 def _update_status(self
):
391 """Get current status"""
394 ret
= self
._client
.status()
398 ret
['repeat'] = int(ret
['repeat'])
399 ret
['random'] = int(ret
['random'])
400 ret
['single'] = int(ret
['single'])
401 ret
['consume'] = int(ret
['consume'])
403 cur
, len = ret
['time'].split(':')
404 ret
['length'] = int(len)
405 ret
['time'] = int(cur
)
410 if not 'songid' in ret
:
414 def __check_command_ok(self
, cmd
):
416 return self
.logger
.info('Not connected.')
417 if not cmd
in self
._commands
:
418 return self
.logger
.error('Command %s not accessible'%cmd
)
421 def __update_outputs(self
):
422 """Update the list of MPD audio outputs."""
423 if self
.__check
_command
_ok
('outputs'):
425 for output
in self
._client
.outputs():
426 outputs
.append(MPClient
.Output(self
, output
['outputname'], output
['outputid'],
427 bool(output
['outputenabled'])))
428 self
.outputs
= outputs
431 def set_output(self
, output_id
, state
):
432 """Set audio output output_id to state (0/1)."""
433 if not self
.__check
_command
_ok
('enableoutput'):
436 self
._client
.enableoutput(output_id
)
438 self
._client
.disableoutput(output_id
)
440 def timerEvent(self
, event
):
441 """Check for changes since last check."""
442 if event
.timerId() == self
._db
_timer
_id
:
443 #timer for monitoring db changes
444 db_update
= self
.stats()['db_update']
445 if db_update
> self
._db
_update
:
446 self
.logger
.info('Database updated.')
447 self
._db
_update
= db_update
448 self
.db_updated
.emit()
452 old_status
= self
._status
453 self
._status
= self
._update
_status
()
456 return self
.disconnect_mpd()
458 if self
._status
['songid'] != old_status
['songid']:
459 self
.__update
_current
_song
()
460 self
.song_changed
.emit(PlaylistEntryRef(self
, self
._status
['songid']))
462 if self
._status
['time'] != old_status
['time']:
463 self
.time_changed
.emit(self
._status
['time'])
465 if self
._status
['state'] != old_status
['state']:
466 self
.state_changed
.emit(self
._status
['state'])
468 if self
._status
['volume'] != old_status
['volume']:
469 self
.volume_changed
.emit( int(self
._status
['volume']))
471 if self
._status
['repeat'] != old_status
['repeat']:
472 self
.repeat_changed
.emit(bool(self
._status
['repeat']))
474 if self
._status
['random'] != old_status
['random']:
475 self
.random_changed
.emit(bool(self
._status
['random']))
477 if self
._status
['single'] != old_status
['single']:
478 self
.single_changed
.emit(bool(self
._status
['single']))
480 if self
._status
['consume'] != old_status
['consume']:
481 self
.consume_changed
.emit(bool(self
._status
['consume']))
483 if self
._status
['playlist'] != old_status
['playlist']:
484 self
.playlist_changed
.emit()
486 outputs
= list(self
._client
.outputs())
487 for i
in range(len(outputs
)):
488 if int(outputs
[i
]['outputenabled']) != int(self
.outputs
[i
].state
):
489 self
.outputs
[i
].mpd_toggle_state()