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."""
31 # these don't change while mpd is running
41 _status
= {'volume' : 0, 'repeat' : 0, 'random' : 0,
42 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
43 'time' : 0, 'length' : 0, 'xfade' : 0,
44 'state' : 'stop', 'single' : 0,
47 _timer_id
= None #for querying status changes
48 _db_timer_id
= None #for querying db updates
49 _db_update
= None #time of last db update
51 __stats
= {'artists': '0', 'albums' : '0', 'songs' : '0', 'uptime' : '0',
52 'playtime' : '0', 'db_playtime' : '0', 'db_update' : '0'}
55 connect_changed
= QtCore
.pyqtSignal(bool)
56 db_updated
= QtCore
.pyqtSignal()
57 song_changed
= QtCore
.pyqtSignal(object)
58 time_changed
= QtCore
.pyqtSignal(int)
59 state_changed
= QtCore
.pyqtSignal(str)
60 volume_changed
= QtCore
.pyqtSignal(int)
61 repeat_changed
= QtCore
.pyqtSignal(bool)
62 random_changed
= QtCore
.pyqtSignal(bool)
63 single_changed
= QtCore
.pyqtSignal(bool)
64 consume_changed
= QtCore
.pyqtSignal(bool)
65 playlist_changed
= QtCore
.pyqtSignal()
69 QtCore
.QObject
.__init
__(self
)
70 self
.logger
= logging
.getLogger('mpclient')
71 self
.__update
_static
()
72 self
._status
= dict(MPClient
._status
)
74 def connect_mpd(self
, host
, port
, password
= None):
75 """Connect to MPD@host:port, optionally using password."""
76 self
.logger
.info('Connecting to MPD...')
78 self
.logger
.warning('Attempted to connect when already connected.')
81 self
._client
= mpd
.MPDClient()
82 self
._client
.connect_changed
.connect(lambda val
:self
.__finish
_connect
() if val
else self
.__finish
_disconnect
())
83 self
._client
.connect_mpd(host
, port
)
84 self
.__password
= password
86 def disconnect_mpd(self
):
87 """Disconnect from MPD."""
88 self
.logger
.info('Disconnecting from MPD...')
90 self
._client
.disconnect_mpd()
92 def password(self
, password
):
93 """Use the password to authenticate with MPD."""
94 self
.logger
.info('Authenticating with MPD.')
95 if not self
.__check
_command
_ok
('password'):
98 self
._client
.password(password
)
99 self
.logger
.info('Successfully authenticated')
100 self
.__update
_static
()
101 except mpd
.CommandError
:
102 self
.logger
.error('Incorrect MPD password.')
103 def is_connected(self
):
104 """Returns True if connected to MPD, False otherwise."""
105 return self
._client
!= None
108 """Get current MPD status."""
110 def playlistinfo(self
):
111 """Returns a list of songs in current playlist."""
112 self
.logger
.info('Listing current playlist.')
113 if not self
.__check
_command
_ok
('playlistinfo'):
115 for song
in self
._client
.playlistinfo():
119 """Returns a list of all songs in library."""
120 self
.logger
.info('Listing library.')
121 if not self
.__check
_command
_ok
('listallinfo'):
123 for song
in self
._client
.listallinfo():
128 def current_song(self
):
129 """Returns the current playing song."""
130 return self
._cur
_song
131 def is_playing(self
):
132 """Returns True if MPD is playing, False otherwise."""
133 return self
._status
['state'] == 'play'
134 def find(self
, *args
):
135 if not self
.__check
_command
_ok
('find'):
137 for song
in self
._client
.find(*args
):
140 def findadd(self
, *args
):
141 """Find tracks with given tags and add them to playlist. Takes
142 a list of (tag, value)."""
143 self
.logger
.info('Findadd %s.'%unicode
(args
))
144 if not self
.__check
_command
_ok
('findadd'):
146 return self
._client
.findadd(*args
)
147 def playlistid(self
, plid
):
148 """Return a song with a given playlist id."""
149 self
.logger
.info('Getting id %s.'%('of id %s'%(plid) if plid
else ''))
150 if not self
.__check
_command
_ok
('play'):
153 for it
in self
._client
.playlistid(plid
):
157 def update_db(self
, paths
= None):
158 """Starts MPD database update."""
159 self
.logger
.info('Updating database %s'%(paths
if paths
else '.'))
160 if not self
.__check
_command
_ok
('update'):
163 return self
._client
.update()
164 self
._client
.command_list_ok_begin()
166 self
._client
.update(path
)
167 list(self
._client
.command_list_end())
170 """Get current volume."""
171 return int(self
._status
['volume'])
172 def set_volume(self
, volume
):
173 """Set volume to volume."""
174 self
.logger
.info('Setting volume to %d.'%volume
)
175 if not self
.__check
_command
_ok
('setvol'):
177 volume
= min(100, max(0, volume
))
179 self
._client
.setvol(volume
)
180 except mpd
.CommandError
, e
:
181 self
.logger
.warning('Error setting volume (probably no outputs enabled): %s.'%e)
184 """Get MPD statistics."""
185 if not self
.__check
_command
_ok
('stats'):
187 return self
._client
.stats()
189 def repeat(self
, val
):
190 """Set repeat playlist to val (True/False)."""
191 self
.logger
.info('Setting repeat to %d.'%val
)
192 if not self
.__check
_command
_ok
('repeat'):
194 if isinstance(val
, bool):
195 val
= 1 if val
else 0
196 self
._client
.repeat(val
)
197 def random(self
, val
):
198 """Set random playback to val (True, False)."""
199 self
.logger
.info('Setting random to %d.'%val
)
200 if not self
.__check
_command
_ok
('random'):
202 if isinstance(val
, bool):
203 val
= 1 if val
else 0
204 self
._client
.random(val
)
205 def crossfade(self
, time
):
206 """Set crossfading between songs."""
207 self
.logger
.info('Setting crossfade to %d'%time
)
208 if not self
.__check
_command
_ok
('crossfade'):
210 self
._client
.crossfade(time
)
211 def single(self
, val
):
212 """Set single playback to val (True, False)"""
213 self
.logger
.info('Setting single to %d.'%val
)
214 if not self
.__check
_command
_ok
('single'):
216 if isinstance(val
, bool):
217 val
= 1 if val
else 0
218 self
._client
.single(val
)
219 def consume(self
, val
):
220 """Set consume mode to val (True, False)"""
221 self
.logger
.info('Setting consume to %d.'%val
)
222 if not self
.__check
_command
_ok
('consume'):
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 self
.logger
.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
231 if not self
.__check
_command
_ok
('play'):
234 self
._client
.playid(id)
236 self
._client
.playid()
239 self
.logger
.info('Pausing playback.')
240 if not self
.__check
_command
_ok
('pause'):
242 self
._client
.pause(1)
244 """Resume playing."""
245 self
.logger
.info('Resuming playback.')
246 if not self
.__check
_command
_ok
('pause'):
248 self
._client
.pause(0)
250 """Move on to the next song in the playlist."""
251 self
.logger
.info('Skipping to next song.')
252 if not self
.__check
_command
_ok
('next'):
256 """Move back to the previous song in the playlist."""
257 self
.logger
.info('Moving to previous song.')
258 if not self
.__check
_command
_ok
('previous'):
260 self
._client
.previous()
263 self
.logger
.info('Stopping playback.')
264 if not self
.__check
_command
_ok
('stop'):
267 def seek(self
, time
):
268 """Seek to time (in seconds)."""
269 self
.logger
.info('Seeking to %d.'%time
)
270 if not self
.__check
_command
_ok
('seekid'):
272 if self
._status
['songid'] > 0:
273 self
._client
.seekid(self
._status
['songid'], time
)
275 def delete(self
, ids
):
276 """Remove all song IDs in list from the playlist."""
277 if not self
.__check
_command
_ok
('deleteid'):
279 self
._client
.command_list_ok_begin()
282 self
.logger
.info('Deleting id %s from playlist.'%id)
283 self
._client
.deleteid(id)
284 list(self
._client
.command_list_end())
285 except mpd
.CommandError
, e
:
286 self
.logger
.error('Error deleting files: %s.'%e)
288 """Clear current playlist."""
289 self
.logger
.info('Clearing playlist.')
290 if not self
.__check
_command
_ok
('clear'):
293 def add(self
, paths
, pos
= -1):
294 """Add all files in paths to the current playlist."""
295 if not self
.__check
_command
_ok
('addid'):
298 self
._client
.command_list_ok_begin()
300 self
.logger
.info('Adding %s to playlist'%path
)
302 self
._client
.addid(path
)
304 self
._client
.addid(path
, pos
)
307 ret
= list(self
._client
.command_list_end())
308 except mpd
.CommandError
, e
:
309 self
.logger
.error('Error adding files: %s.'%e)
310 if self
._status
['state'] == 'stop' and ret
:
312 def move(self
, source
, target
):
313 """Move the songs in playlist. Takes one source id and one target position."""
314 self
.logger
.info('Moving %s to %s.'%(source
, target
))
315 if not self
.__check
_command
_ok
('moveid'):
317 self
._client
.moveid(source
, target
)
320 def __finish_connect(self
):
322 self
.password(self
.__password
)
324 self
.__update
_static
()
326 if not self
.__check
_command
_ok
('listallinfo'):
327 self
.logger
.error('Don\'t have MPD read permission, diconnecting.')
328 return self
.disconnect_mpd()
330 self
.__update
_current
_song
()
331 self
._db
_update
= self
.stats()['db_update']
333 self
.connect_changed
.emit(True)
334 self
.logger
.info('Successfully connected to MPD.')
335 self
._timer
_id
= self
.startTimer(500)
336 self
._db
_timer
_id
= self
.startTimer(1000)
337 def __finish_disconnect(self
):
341 self
.killTimer(self
._timer
_id
)
342 self
._timer
_id
= None
343 if self
._db
_timer
_id
:
344 self
.killTimer(self
._db
_timer
_id
)
345 self
._db
_timer
_id
= None
346 self
._status
= dict(MPClient
._status
)
347 self
._cur
_song
= None
348 self
.__update
_static
()
349 self
.connect_changed
.emit(False)
350 self
.logger
.info('Disconnected from MPD.')
351 def __update_current_song(self
):
352 """Update the current song."""
353 song
= self
._client
.currentsong()
355 self
._cur
_song
= None
357 self
._cur
_song
= Song(song
)
358 def _update_status(self
):
359 """Get current status"""
362 ret
= self
._client
.status()
366 ret
['repeat'] = int(ret
['repeat'])
367 ret
['random'] = int(ret
['random'])
368 ret
['single'] = int(ret
['single'])
369 ret
['consume'] = int(ret
['consume'])
370 ret
['volume'] = int(ret
['volume'])
372 cur
, len = ret
['time'].split(':')
373 ret
['length'] = int(len)
374 ret
['time'] = int(cur
)
379 if not 'songid' in ret
:
383 def __check_command_ok(self
, cmd
):
385 return self
.logger
.info('Not connected.')
386 if not cmd
in self
.commands
:
387 return self
.logger
.error('Command %s not accessible'%cmd
)
390 def __update_static(self
):
391 """Update static values, called on connect/disconnect."""
393 self
.commands
= list(self
._client
.commands())
397 if self
.__check
_command
_ok
('outputs'):
399 for output
in self
._client
.outputs():
400 outputs
.append(AudioOutput(self
, output
['outputname'], output
['outputid'],
401 bool(output
['outputenabled'])))
402 self
.outputs
= outputs
406 if self
.__check
_command
_ok
('tagtypes'):
407 self
.tagtypes
= map(unicode.lower
, self
._client
.tagtypes()) + ['file']
411 if self
.__check
_command
_ok
('urlhandlers'):
412 self
.urlhandlers
= list(self
._client
.urlhandlers())
414 self
.urlhandlers
= []
416 def set_output(self
, output_id
, state
):
417 """Set audio output output_id to state (0/1). Called only by AudioOutput."""
418 if not self
.__check
_command
_ok
('enableoutput'):
421 self
._client
.enableoutput(output_id
)
423 self
._client
.disableoutput(output_id
)
425 def timerEvent(self
, event
):
426 """Check for changes since last check."""
427 if event
.timerId() == self
._db
_timer
_id
:
428 #timer for monitoring db changes
429 db_update
= self
.stats()['db_update']
430 if db_update
> self
._db
_update
:
431 self
.logger
.info('Database updated.')
432 self
._db
_update
= db_update
433 self
.db_updated
.emit()
437 old_status
= self
._status
438 self
._status
= self
._update
_status
()
441 self
.logger
.error('Error reading status.')
442 return self
.disconnect_mpd()
444 if self
._status
['songid'] != old_status
['songid']:
445 self
.__update
_current
_song
()
446 self
.song_changed
.emit(PlaylistEntryRef(self
, self
._status
['songid']))
448 if self
._status
['time'] != old_status
['time']:
449 self
.time_changed
.emit(self
._status
['time'])
451 if self
._status
['state'] != old_status
['state']:
452 self
.state_changed
.emit(self
._status
['state'])
454 if self
._status
['volume'] != old_status
['volume']:
455 self
.volume_changed
.emit( int(self
._status
['volume']))
457 if self
._status
['repeat'] != old_status
['repeat']:
458 self
.repeat_changed
.emit(bool(self
._status
['repeat']))
460 if self
._status
['random'] != old_status
['random']:
461 self
.random_changed
.emit(bool(self
._status
['random']))
463 if self
._status
['single'] != old_status
['single']:
464 self
.single_changed
.emit(bool(self
._status
['single']))
466 if self
._status
['consume'] != old_status
['consume']:
467 self
.consume_changed
.emit(bool(self
._status
['consume']))
469 if self
._status
['playlist'] != old_status
['playlist']:
470 self
.playlist_changed
.emit()
472 outputs
= list(self
._client
.outputs())
473 for i
in range(len(outputs
)):
474 if int(outputs
[i
]['outputenabled']) != int(self
.outputs
[i
].state
):
475 self
.outputs
[i
].mpd_toggle_state()
478 class AudioOutput(QtCore
.QObject
):
479 """This class represents an MPD audio output."""
487 state_changed
= QtCore
.pyqtSignal(bool)
490 def __init__(self
, mpclient
, name
, id, state
):
491 QtCore
.QObject
.__init
__(self
)
493 self
.mpclient
= mpclient
498 @QtCore.pyqtSlot(bool)
499 def set_state(self
, state
):
500 self
.mpclient
.set_output(self
.id, state
)
503 def mpd_toggle_state(self
):
504 """This is called by mpclient to inform about output state change."""
505 self
.state
= not self
.state
506 self
.state_changed
.emit(self
.state
)