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."""
30 _status
= {'volume' : 0, 'repeat' : 0, 'random' : 0,
31 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
32 'time' : 0, 'length' : 0, 'xfade' : 0,
33 'state' : 'stop', 'single' : 0,
37 _timer_id
= None #for querying status changes
38 _db_timer_id
= None #for querying db updates
39 _db_update
= None #time of last db update
41 _retr_mutex
= QtCore
.QMutex()
46 connect_changed
= QtCore
.pyqtSignal(bool)
47 db_updated
= QtCore
.pyqtSignal()
48 song_changed
= QtCore
.pyqtSignal(str)
49 time_changed
= QtCore
.pyqtSignal(int)
50 state_changed
= QtCore
.pyqtSignal(str)
51 volume_changed
= QtCore
.pyqtSignal(int)
52 repeat_changed
= QtCore
.pyqtSignal(bool)
53 random_changed
= QtCore
.pyqtSignal(bool)
54 single_changed
= QtCore
.pyqtSignal(bool)
55 consume_changed
= QtCore
.pyqtSignal(bool)
56 playlist_changed
= QtCore
.pyqtSignal()
59 QtCore
.QObject
.__init
__(self
)
61 self
._status
= dict(MPClient
._status
)
62 self
.logger
= logging
.getLogger('mpclient')
64 def connect_mpd(self
, host
, port
, password
= None):
65 """Connect to MPD@host:port, optionally using password.
66 Returns True at success, False otherwise."""
68 self
.logger
.info('Connecting to MPD...')
70 self
.logger
.warning('Attempted to connect when already connected.')
74 self
._client
= mpd
.MPDClient()
75 self
._client
.connect(host
, port
)
76 except socket
.error
, e
:
77 self
.logger
.error('Socket error: %s.'%e)
82 self
.password(password
)
84 self
._commands
= self
._retrieve
(self
._client
.commands
)
86 if not self
.__check
_command
_ok
('listallinfo'):
87 self
.logger
.error('Don\'t have MPD read permission, diconnecting.')
88 return self
.disconnect_mpd()
90 self
._update
_current
_song
()
91 self
._db
_update
= self
.stats()['db_update']
93 self
.emit(QtCore
.SIGNAL('connected')) #should be removed
94 self
.connect_changed
.emit(True)
95 self
.logger
.info('Successfully connected to MPD.')
96 self
._timer
_id
= self
.startTimer(500)
97 self
._db
_timer
_id
= self
.startTimer(10000)
99 def disconnect_mpd(self
):
100 """Disconnect from MPD."""
101 self
.logger
.info('Disconnecting from MPD...')
105 self
._client
.disconnect()
106 except (mpd
.ConnectionError
, socket
.error
):
110 self
.logger
.warning('Attempted to disconnect when not connected.')
113 self
.killTimer(self
._timer
_id
)
114 self
._timer
_id
= None
115 if self
._db
_timer
_id
:
116 self
.killTimer(self
._db
_timer
_id
)
117 self
._db
_timer
_id
= None
118 self
._status
= dict(MPClient
._status
)
119 self
._cur
_song
= None
121 self
.emit(QtCore
.SIGNAL('disconnected')) #should be removed
122 self
.connect_changed
.emit(False)
123 self
.logger
.info('Disconnected from MPD.')
124 def password(self
, password
):
125 """Use the password to authenticate with MPD."""
126 self
.logger
.info('Authenticating with MPD.')
127 if not self
.__check
_command
_ok
('password'):
130 self
._client
.password(password
)
131 self
.logger
.info('Successfully authenticated')
132 self
._commands
= self
._retrieve
(self
._client
.commands
)
133 except mpd
.CommandError
:
134 self
.logger
.error('Incorrect MPD password.')
135 def is_connected(self
):
136 """Returns True if connected to MPD, False otherwise."""
137 return self
._client
!= None
140 """Get current MPD status."""
143 """Returns a list of songs in current playlist."""
144 self
.logger
.info('Listing current playlist.')
145 if not self
.__check
_command
_ok
('playlistinfo'):
147 return self
._array
_to
_song
_array
(self
._retrieve
(self
._client
.playlistinfo
))
149 """Returns a list of all songs in library."""
150 self
.logger
.info('Listing library.')
151 if not self
.__check
_command
_ok
('listallinfo'):
153 return self
._array
_to
_song
_array
(self
._retrieve
(self
._client
.listallinfo
))
154 def current_song(self
):
155 """Returns the current playing song."""
156 return self
._cur
_song
157 def is_playing(self
):
158 """Returns True if MPD is playing, False otherwise."""
159 return self
._status
['state'] == 'play'
160 def find(self
, path
):
161 if not self
.__check
_command
_ok
('find'):
163 return self
._client
.find('file', path
.encode('utf-8'))
165 def update_db(self
, paths
= None):
166 """Starts MPD database update."""
167 self
.logger
.info('Updating database %s'%(paths
if paths
else '.'))
168 if not self
.__check
_command
_ok
('update'):
171 return self
._client
.update()
172 self
._client
.command_list_ok_begin()
174 self
._client
.update(path
)
175 self
._client
.command_list_end()
178 """Returns an array of configured MPD audio outputs."""
180 return self
._retrieve
(self
._client
.outputs
)
183 def set_output(self
, output_id
, state
):
184 """Set audio output output_id to state (0/1)."""
185 if not self
.__check
_command
_ok
('enableoutput'):
188 self
._client
.enableoutput(output_id
)
190 self
._client
.disableoutput(output_id
)
193 """Get current volume."""
194 return int(self
._status
['volume'])
195 def set_volume(self
, volume
):
196 """Set volume to volume."""
197 self
.logger
.info('Setting volume to %d.'%volume
)
198 if not self
.__check
_command
_ok
('setvol'):
200 volume
= min(100, max(0, volume
))
201 self
._client
.setvol(volume
)
203 def urlhandlers(self
):
204 """Returns an array of available url handlers."""
208 return self
._client
.urlhandlers()
210 """Returns a list of supported tags."""
211 if not self
.__check
_command
_ok
('tagtypes'):
214 return self
._retrieve
(self
._client
.tagtypes
)
216 """List all currently available MPD commands."""
217 return self
._commands
219 """Get MPD statistics."""
220 return self
._retrieve
(self
._client
.stats
)
222 def repeat(self
, val
):
223 """Set repeat playlist to val (True/False)."""
224 self
.logger
.info('Setting repeat to %d.'%val
)
225 if not self
.__check
_command
_ok
('repeat'):
227 if isinstance(val
, bool):
228 val
= 1 if val
else 0
229 self
._client
.repeat(val
)
230 def random(self
, val
):
231 """Set random playback to val (True, False)."""
232 self
.logger
.info('Setting random to %d.'%val
)
233 if not self
.__check
_command
_ok
('random'):
235 if isinstance(val
, bool):
236 val
= 1 if val
else 0
237 self
._client
.random(val
)
238 def crossfade(self
, time
):
239 """Set crossfading between songs."""
240 self
.logger
.info('Setting crossfade to %d'%time
)
241 if not self
.__check
_command
_ok
('crossfade'):
243 self
._client
.crossfade(time
)
244 def single(self
, val
):
245 """Set single playback to val (True, False)"""
246 self
.logger
.info('Setting single to %d.'%val
)
247 if not self
.__check
_command
_ok
('single'):
249 if isinstance(val
, bool):
250 val
= 1 if val
else 0
251 self
._client
.single(val
)
252 def consume(self
, val
):
253 """Set consume mode to val (True, False)"""
254 self
.logger
.info('Setting consume to %d.'%val
)
255 if not self
.__check
_command
_ok
('consume'):
257 if isinstance(val
, bool):
258 val
= 1 if val
else 0
259 self
._client
.consume(val
)
261 def play(self
, id = None):
262 """Play song with ID id or next song if id is None."""
263 self
.logger
.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
264 if not self
.__check
_command
_ok
('play'):
267 self
._client
.playid(id)
269 self
._client
.playid()
272 self
.logger
.info('Pausing playback.')
273 if not self
.__check
_command
_ok
('pause'):
275 self
._client
.pause(1)
277 """Resume playing."""
278 self
.logger
.info('Resuming playback.')
279 if not self
.__check
_command
_ok
('pause'):
281 self
._client
.pause(0)
283 """Move on to the next song in the playlist."""
284 self
.logger
.info('Skipping to next song.')
285 if not self
.__check
_command
_ok
('next'):
289 """Move back to the previous song in the playlist."""
290 self
.logger
.info('Moving to previous song.')
291 if not self
.__check
_command
_ok
('previous'):
293 self
._client
.previous()
296 self
.logger
.info('Stopping playback.')
297 if not self
.__check
_command
_ok
('stop'):
300 def seek(self
, time
):
301 """Seek to time (in seconds)."""
302 self
.logger
.info('Seeking to %d.'%time
)
303 if not self
.__check
_command
_ok
('seekid'):
305 if self
._status
['songid'] > 0:
306 self
._client
.seekid(self
._status
['songid'], time
)
308 def delete(self
, list):
309 """Remove all song IDs in list from the playlist."""
310 if not self
.__check
_command
_ok
('deleteid'):
312 self
._client
.command_list_ok_begin()
315 self
.logger
.info('Deleting id %s from playlist.'%id)
316 self
._client
.deleteid(id)
317 self
._client
.command_list_end()
318 except mpd
.CommandError
, e
:
319 self
.logger
.error('Error deleting files: %s.'%e)
321 """Clear current playlist."""
322 self
.logger
.info('Clearing playlist.')
323 if not self
.__check
_command
_ok
('clear'):
326 def add(self
, paths
):
327 """Add all files in paths to the current playlist."""
328 if not self
.__check
_command
_ok
('addid'):
331 self
._client
.command_list_ok_begin()
334 self
.logger
.info('Adding %s to playlist'%path
)
335 self
._client
.addid(path
.encode('utf-8'))
336 ret
= self
._client
.command_list_end()
337 except mpd
.CommandError
, e
:
338 self
.logger
.error('Error adding files: %s.'%e)
339 if self
._status
['state'] == 'stop' and ret
:
341 def move(self
, source
, target
):
342 """Move the songs in playlist. Takes a list of source ids and one target position."""
343 self
.logger
.info('Moving %d to %d.'%(source
, target
))
344 if not self
.__check
_command
_ok
('moveid'):
346 self
._client
.command_list_ok_begin()
349 self
._client
.moveid(id, target
+ i
)
351 self
._client
.command_list_end()
353 def _retrieve(self
, method
):
354 """Makes sure only one call is made at a time to MPD."""
355 self
._retr
_mutex
.lock()
359 self
.logger
.error('Connection to MPD broken.')
360 self
._retr
_mutex
.unlock()
361 self
.disconnect_mpd()
364 self
._retr
_mutex
.unlock()
366 def _array_to_song_array(self
, array
):
367 """Convert an array to an array of Songs."""
368 return map(lambda entry
: Song(entry
)
369 , filter(lambda entry
: not('directory' in entry
), array
)
371 def _update_current_song(self
):
372 """Update the current song."""
373 song
= self
._retrieve
(self
._client
.currentsong
)
375 self
._cur
_song
= None
377 self
._cur
_song
= Song(song
)
378 def _update_status(self
):
379 """Get current status"""
382 ret
= self
._retrieve
(self
._client
.status
)
386 ret
['repeat'] = int(ret
['repeat'])
387 ret
['random'] = int(ret
['random'])
388 ret
['single'] = int(ret
['single'])
389 ret
['consume'] = int(ret
['consume'])
391 cur
, len = ret
['time'].split(':')
392 ret
['length'] = int(len)
393 ret
['time'] = int(cur
)
398 if not 'songid' in ret
:
402 def __check_command_ok(self
, cmd
):
404 return self
.logger
.info('Not connected.')
405 if not cmd
in self
._commands
:
406 return self
.logger
.error('Command %s not accessible'%cmd
)
409 def timerEvent(self
, event
):
410 """Check for changes since last check."""
411 if event
.timerId() == self
._db
_timer
_id
:
412 #timer for monitoring db changes
413 db_update
= self
.stats()['db_update']
414 if db_update
> self
._db
_update
:
415 self
.logger
.info('Database updated.')
416 self
._db
_update
= db_update
417 self
.db_updated
.emit()
421 old_status
= self
._status
422 self
._status
= self
._update
_status
()
425 return self
.disconnect_mpd()
427 self
._update
_current
_song
()
429 if self
._status
['songid'] != old_status
['songid']:
430 self
.song_changed
.emit(self
._status
['songid'])
432 if self
._status
['time'] != old_status
['time']:
433 self
.time_changed
.emit(self
._status
['time'])
435 if self
._status
['state'] != old_status
['state']:
436 self
.state_changed
.emit(self
._status
['state'])
438 if self
._status
['volume'] != old_status
['volume']:
439 self
.volume_changed
.emit( int(self
._status
['volume']))
441 if self
._status
['repeat'] != old_status
['repeat']:
442 self
.repeat_changed
.emit(bool(self
._status
['repeat']))
444 if self
._status
['random'] != old_status
['random']:
445 self
.random_changed
.emit(bool(self
._status
['random']))
447 if self
._status
['single'] != old_status
['single']:
448 self
.single_changed
.emit(bool(self
._status
['single']))
450 if self
._status
['consume'] != old_status
['consume']:
451 self
.consume_changed
.emit(bool(self
._status
['consume']))
453 if self
._status
['playlist'] != old_status
['playlist']:
454 self
.playlist_changed
.emit()