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
, QtNetwork
20 from PyQt4
.QtCore
import pyqtSignal
as Signal
, pyqtSlot
as Slot
25 from song
import Song
, PlaylistEntryRef
26 from mpdsocket
import MPDSocket
28 class MPClient(QtCore
.QObject
):
29 """This class offers another layer above pympd, with usefull events."""
33 # these don't change while mpd is running
43 _status
= {'volume' : 0, 'repeat' : 0, 'random' : 0,
44 'songid' : 0, 'playlist' : 0, 'playlistlength' : 0,
45 'time' : 0, 'length' : 0, 'xfade' : 0,
46 'state' : 'stop', 'single' : 0,
49 _timer_id
= None #for querying status changes
50 _db_timer_id
= None #for querying db updates
51 _db_update
= None #time of last db update
53 __stats
= {'artists': '0', 'albums' : '0', 'songs' : '0', 'uptime' : '0',
54 'playtime' : '0', 'db_playtime' : '0', 'db_update' : '0'}
57 connect_changed
= QtCore
.pyqtSignal(bool)
58 db_updated
= QtCore
.pyqtSignal()
59 song_changed
= QtCore
.pyqtSignal(object)
60 time_changed
= QtCore
.pyqtSignal(int)
61 state_changed
= QtCore
.pyqtSignal(str)
62 volume_changed
= QtCore
.pyqtSignal(int)
63 repeat_changed
= QtCore
.pyqtSignal(bool)
64 random_changed
= QtCore
.pyqtSignal(bool)
65 single_changed
= QtCore
.pyqtSignal(bool)
66 consume_changed
= QtCore
.pyqtSignal(bool)
67 playlist_changed
= QtCore
.pyqtSignal()
71 QtCore
.QObject
.__init
__(self
)
72 self
.logger
= logging
.getLogger('mpclient')
73 self
.__update
_static
()
74 self
._status
= dict(MPClient
._status
)
76 def connect_mpd(self
, host
, port
, password
= None):
77 """Connect to MPD@host:port, optionally using password."""
78 self
.logger
.info('Connecting to MPD...')
80 self
.logger
.warning('Attempted to connect when already connected.')
83 self
._client
= mpd
.MPDClient()
84 self
._client
.connect_changed
.connect(lambda val
:self
.__finish
_connect
() if val
else self
.__finish
_disconnect
())
85 self
._client
.connect_mpd(host
, port
)
86 self
.__password
= password
88 def disconnect_mpd(self
):
89 """Disconnect from MPD."""
90 self
.logger
.info('Disconnecting from MPD...')
92 self
._client
.disconnect_mpd()
94 def password(self
, password
):
95 """Use the password to authenticate with MPD."""
96 self
.logger
.info('Authenticating with MPD.')
97 if not self
.__check
_command
_ok
('password'):
100 self
._client
.password(password
)
101 self
.logger
.info('Successfully authenticated')
102 self
.__update
_static
()
103 except mpd
.CommandError
:
104 self
.logger
.error('Incorrect MPD password.')
105 def is_connected(self
):
106 """Returns True if connected to MPD, False otherwise."""
107 return self
._client
!= None
110 """Get current MPD status."""
112 def playlistinfo(self
):
113 """Returns a list of songs in current playlist."""
114 self
.logger
.info('Listing current playlist.')
115 if not self
.__check
_command
_ok
('playlistinfo'):
117 for song
in self
._client
.playlistinfo():
121 """Returns a list of all songs in library."""
122 self
.logger
.info('Listing library.')
123 if not self
.__check
_command
_ok
('listallinfo'):
125 for song
in self
._client
.listallinfo():
130 def current_song(self
):
131 """Returns the current playing song."""
132 return self
._cur
_song
133 def is_playing(self
):
134 """Returns True if MPD is playing, False otherwise."""
135 return self
._status
['state'] == 'play'
136 def find(self
, *args
):
137 if not self
.__check
_command
_ok
('find'):
139 for song
in self
._client
.find(*args
):
142 def findadd(self
, *args
):
143 """Find tracks with given tags and add them to playlist. Takes
144 a list of (tag, value)."""
145 self
.logger
.info('Findadd %s.'%unicode
(args
))
146 if not self
.__check
_command
_ok
('findadd'):
148 return self
._client
.findadd(*args
)
149 def playlistid(self
, plid
):
150 """Return a song with a given playlist id."""
151 self
.logger
.info('Getting id %s.'%('of id %s'%(plid) if plid
else ''))
152 if not self
.__check
_command
_ok
('play'):
155 for it
in self
._client
.playlistid(plid
):
159 def update_db(self
, paths
= None):
160 """Starts MPD database update."""
161 self
.logger
.info('Updating database %s'%(paths
if paths
else '.'))
162 if not self
.__check
_command
_ok
('update'):
165 return self
._client
.update()
166 self
._client
.command_list_ok_begin()
168 self
._client
.update(path
)
169 list(self
._client
.command_list_end())
172 """Get current volume."""
173 return int(self
._status
['volume'])
174 def set_volume(self
, volume
):
175 """Set volume to volume."""
176 self
.logger
.info('Setting volume to %d.'%volume
)
177 if not self
.__check
_command
_ok
('setvol'):
179 volume
= min(100, max(0, volume
))
181 self
._client
.setvol(volume
)
182 except mpd
.CommandError
, e
:
183 self
.logger
.warning('Error setting volume (probably no outputs enabled): %s.'%e)
186 """Get MPD statistics."""
187 if not self
.__check
_command
_ok
('stats'):
189 return self
._client
.stats()
191 def repeat(self
, val
):
192 """Set repeat playlist to val (True/False)."""
193 self
.logger
.info('Setting repeat to %d.'%val
)
194 if not self
.__check
_command
_ok
('repeat'):
196 if isinstance(val
, bool):
197 val
= 1 if val
else 0
198 self
._client
.repeat(val
)
199 def random(self
, val
):
200 """Set random playback to val (True, False)."""
201 self
.logger
.info('Setting random to %d.'%val
)
202 if not self
.__check
_command
_ok
('random'):
204 if isinstance(val
, bool):
205 val
= 1 if val
else 0
206 self
._client
.random(val
)
207 def crossfade(self
, time
):
208 """Set crossfading between songs."""
209 self
.logger
.info('Setting crossfade to %d'%time
)
210 if not self
.__check
_command
_ok
('crossfade'):
212 self
._client
.crossfade(time
)
213 def single(self
, val
):
214 """Set single playback to val (True, False)"""
215 self
.logger
.info('Setting single to %d.'%val
)
216 if not self
.__check
_command
_ok
('single'):
218 if isinstance(val
, bool):
219 val
= 1 if val
else 0
220 self
._client
.single(val
)
221 def consume(self
, val
):
222 """Set consume mode to val (True, False)"""
223 self
.logger
.info('Setting consume to %d.'%val
)
224 if not self
.__check
_command
_ok
('consume'):
226 if isinstance(val
, bool):
227 val
= 1 if val
else 0
228 self
._client
.consume(val
)
230 def play(self
, id = None):
231 """Play song with ID id or next song if id is None."""
232 self
.logger
.info('Starting playback %s.'%('of id %s'%(id) if id else ''))
233 if not self
.__check
_command
_ok
('play'):
236 self
._client
.playid(id)
238 self
._client
.playid()
241 self
.logger
.info('Pausing playback.')
242 if not self
.__check
_command
_ok
('pause'):
244 self
._client
.pause(1)
246 """Resume playing."""
247 self
.logger
.info('Resuming playback.')
248 if not self
.__check
_command
_ok
('pause'):
250 self
._client
.pause(0)
252 """Move on to the next song in the playlist."""
253 self
.logger
.info('Skipping to next song.')
254 if not self
.__check
_command
_ok
('next'):
258 """Move back to the previous song in the playlist."""
259 self
.logger
.info('Moving to previous song.')
260 if not self
.__check
_command
_ok
('previous'):
262 self
._client
.previous()
265 self
.logger
.info('Stopping playback.')
266 if not self
.__check
_command
_ok
('stop'):
269 def seek(self
, time
):
270 """Seek to time (in seconds)."""
271 self
.logger
.info('Seeking to %d.'%time
)
272 if not self
.__check
_command
_ok
('seekid'):
274 if self
._status
['songid'] > 0:
275 self
._client
.seekid(self
._status
['songid'], time
)
277 def delete(self
, ids
):
278 """Remove all song IDs in list from the playlist."""
279 if not self
.__check
_command
_ok
('deleteid'):
281 self
._client
.command_list_ok_begin()
284 self
.logger
.info('Deleting id %s from playlist.'%id)
285 self
._client
.deleteid(id)
286 list(self
._client
.command_list_end())
287 except mpd
.CommandError
, e
:
288 self
.logger
.error('Error deleting files: %s.'%e)
290 """Clear current playlist."""
291 self
.logger
.info('Clearing playlist.')
292 if not self
.__check
_command
_ok
('clear'):
295 def add(self
, paths
, pos
= -1):
296 """Add all files in paths to the current playlist."""
297 if not self
.__check
_command
_ok
('addid'):
300 self
._client
.command_list_ok_begin()
302 self
.logger
.info('Adding %s to playlist'%path
)
304 self
._client
.addid(path
)
306 self
._client
.addid(path
, pos
)
309 ret
= list(self
._client
.command_list_end())
310 except mpd
.CommandError
, e
:
311 self
.logger
.error('Error adding files: %s.'%e)
312 if self
._status
['state'] == 'stop' and ret
:
314 def move(self
, source
, target
):
315 """Move the songs in playlist. Takes one source id and one target position."""
316 self
.logger
.info('Moving %s to %s.'%(source
, target
))
317 if not self
.__check
_command
_ok
('moveid'):
319 self
._client
.moveid(source
, target
)
322 def __finish_connect(self
):
324 self
.password(self
.__password
)
326 self
.__update
_static
()
328 if not self
.__check
_command
_ok
('listallinfo'):
329 self
.logger
.error('Don\'t have MPD read permission, diconnecting.')
330 return self
.disconnect_mpd()
332 self
.__update
_current
_song
()
333 self
._db
_update
= self
.stats()['db_update']
335 self
.connect_changed
.emit(True)
336 self
.logger
.info('Successfully connected to MPD.')
337 self
._timer
_id
= self
.startTimer(500)
338 self
._db
_timer
_id
= self
.startTimer(1000)
339 def __finish_disconnect(self
):
343 self
.killTimer(self
._timer
_id
)
344 self
._timer
_id
= None
345 if self
._db
_timer
_id
:
346 self
.killTimer(self
._db
_timer
_id
)
347 self
._db
_timer
_id
= None
348 self
._status
= dict(MPClient
._status
)
349 self
._cur
_song
= None
350 self
.__update
_static
()
351 self
.connect_changed
.emit(False)
352 self
.logger
.info('Disconnected from MPD.')
353 def __update_current_song(self
):
354 """Update the current song."""
355 song
= self
._client
.currentsong()
357 self
._cur
_song
= None
359 self
._cur
_song
= Song(song
)
360 def _update_status(self
):
361 """Get current status"""
364 ret
= self
._client
.status()
368 ret
['repeat'] = int(ret
['repeat'])
369 ret
['random'] = int(ret
['random'])
370 ret
['single'] = int(ret
['single'])
371 ret
['consume'] = int(ret
['consume'])
372 ret
['volume'] = int(ret
['volume'])
374 cur
, len = ret
['time'].split(':')
375 ret
['length'] = int(len)
376 ret
['time'] = int(cur
)
381 if not 'songid' in ret
:
385 def __check_command_ok(self
, cmd
):
387 return self
.logger
.info('Not connected.')
388 if not cmd
in self
.commands
:
389 return self
.logger
.error('Command %s not accessible'%cmd
)
392 def __update_static(self
):
393 """Update static values, called on connect/disconnect."""
395 self
.commands
= list(self
._client
.commands())
399 if self
.__check
_command
_ok
('outputs'):
401 for output
in self
._client
.outputs():
402 outputs
.append(AudioOutput(self
, output
['outputname'], output
['outputid'],
403 bool(output
['outputenabled'])))
404 self
.outputs
= outputs
408 if self
.__check
_command
_ok
('tagtypes'):
409 self
.tagtypes
= map(unicode.lower
, self
._client
.tagtypes()) + ['file']
413 if self
.__check
_command
_ok
('urlhandlers'):
414 self
.urlhandlers
= list(self
._client
.urlhandlers())
416 self
.urlhandlers
= []
418 def set_output(self
, output_id
, state
):
419 """Set audio output output_id to state (0/1). Called only by AudioOutput."""
420 if not self
.__check
_command
_ok
('enableoutput'):
423 self
._client
.enableoutput(output_id
)
425 self
._client
.disableoutput(output_id
)
427 def timerEvent(self
, event
):
428 """Check for changes since last check."""
429 if event
.timerId() == self
._db
_timer
_id
:
430 #timer for monitoring db changes
431 db_update
= self
.stats()['db_update']
432 if db_update
> self
._db
_update
:
433 self
.logger
.info('Database updated.')
434 self
._db
_update
= db_update
435 self
.db_updated
.emit()
439 old_status
= self
._status
440 self
._status
= self
._update
_status
()
443 self
.logger
.error('Error reading status.')
444 return self
.disconnect_mpd()
446 if self
._status
['songid'] != old_status
['songid']:
447 self
.__update
_current
_song
()
448 self
.song_changed
.emit(PlaylistEntryRef(self
, self
._status
['songid']))
450 if self
._status
['time'] != old_status
['time']:
451 self
.time_changed
.emit(self
._status
['time'])
453 if self
._status
['state'] != old_status
['state']:
454 self
.state_changed
.emit(self
._status
['state'])
456 if self
._status
['volume'] != old_status
['volume']:
457 self
.volume_changed
.emit( int(self
._status
['volume']))
459 if self
._status
['repeat'] != old_status
['repeat']:
460 self
.repeat_changed
.emit(bool(self
._status
['repeat']))
462 if self
._status
['random'] != old_status
['random']:
463 self
.random_changed
.emit(bool(self
._status
['random']))
465 if self
._status
['single'] != old_status
['single']:
466 self
.single_changed
.emit(bool(self
._status
['single']))
468 if self
._status
['consume'] != old_status
['consume']:
469 self
.consume_changed
.emit(bool(self
._status
['consume']))
471 if self
._status
['playlist'] != old_status
['playlist']:
472 self
.playlist_changed
.emit()
474 outputs
= list(self
._client
.outputs())
475 for i
in range(len(outputs
)):
476 if int(outputs
[i
]['outputenabled']) != int(self
.outputs
[i
].state
):
477 self
.outputs
[i
].mpd_toggle_state()
480 class AudioOutput(QtCore
.QObject
):
481 """This class represents an MPD audio output."""
489 state_changed
= QtCore
.pyqtSignal(bool)
492 def __init__(self
, mpclient
, name
, id, state
):
493 QtCore
.QObject
.__init
__(self
)
495 self
.mpclient
= mpclient
500 @QtCore.pyqtSlot(bool)
501 def set_state(self
, state
):
502 self
.mpclient
.set_output(self
.id, state
)
505 def mpd_toggle_state(self
):
506 """This is called by mpclient to inform about output state change."""
507 self
.state
= not self
.state
508 self
.state_changed
.emit(self
.state
)
510 class AudioOutput2(QtCore
.QObject
):
511 """This class represents an MPD audio output."""
521 state_changed
= QtCore
.pyqtSignal(bool)
524 def __init__(self
, data
, set_state
, parent
= None):
525 QtCore
.QObject
.__init
__(self
, parent
)
527 self
.name
= data
['outputname']
528 self
.state
= int(data
['outputenabled'])
530 self
.set_state
= set_state
533 def set_state(self
, state
):
536 def update(self
, data
):
538 This is called by mpclient to inform about output state change.
540 if int(data
['outputenabled']) != self
.state
:
541 self
.state
= not self
.state
542 self
.state_changed
.emit(self
.state
)
544 class MPDStatus(dict):
545 _status
= {'volume' : 0, 'repeat' : 0, 'single' : 0,
546 'consume' : 0, 'playlist' : '-1', 'playlistlength' : 0,
547 'state' : 'stop', 'song' : -1, 'songid' : '-1',
548 'nextsong' : -1, 'nextsongid' : '-1', 'time' : '0:0',
549 'elapsed' : .0, 'bitrate' : 0, 'xfade' : 0,
550 'mixrampdb' : .0, 'mixrampdelay' : .0, 'audio' : '0:0:0',
551 'updatings_db' : -1, 'error' : '', 'random' : 0 }
553 def __init__(self
, data
= {}):
554 dict.__init
__(self
, MPDStatus
._status
)
556 if key
in self
._status
:
557 self
[key
] = type(self
._status
[key
])(data
[key
])
559 self
[key
] = data
[key
]
561 self
['time'] = map(int, self
['time'].split(':'))
563 self
['time'] = [0, 0]
565 self
['audio'] = tuple(map(int, self
['audio'].split(':')))
567 self
['audio'] = (0, 0, 0)
569 class MPClient2(QtCore
.QObject
):
571 A high-level MPD interface. It is mostly asynchronous -- all responses from
572 MPD are read via callbacks. A callback may be None, in which case the data
573 is silently discarded. Callbacks that take iterators must ensure that the
574 iterator is exhausted.
578 # these don't change while we are connected
579 """A list of AudioOutputs available."""
581 """A list of supported tags (valid indices for Song)."""
583 """A list of supported URL handlers."""
587 """An MPDStatus object representing current status."""
589 """A Song object representing current song."""
593 connect_changed
= Signal(bool)
594 db_updated
= Signal()
595 time_changed
= Signal(int)
596 song_changed
= Signal(object)
597 state_changed
= Signal(str)
598 volume_changed
= Signal(int)
599 repeat_changed
= Signal(bool)
600 random_changed
= Signal(bool)
601 single_changed
= Signal(bool)
602 consume_changed
= Signal(bool)
603 playlist_changed
= Signal()
608 _sup_ver
= (0, 16, 0)
612 # these don't change while we are connected
618 def __init__(self
, parent
= None):
619 QtCore
.QObject
.__init
__(self
, parent
)
620 self
._logger
= logging
.getLogger('%smpclient'%(unicode(parent
) + "." if parent
else ""))
621 self
._timer
= QtCore
.QTimer(self
)
622 self
._timer
.setInterval(1000)
623 self
._timer
.timeout
.connect(self
._update
_timer
)
624 self
._socket
= MPDSocket(self
)
626 self
.status
= MPDStatus()
627 self
.cur_song
= Song()
630 self
.urlhandlers
= []
633 return self
._logger
.name
635 def connect_mpd(self
, host
= "localhost", port
= 6600, password
= None):
637 Connect to MPD at host:port optionally using a password. A Unix domain
638 socket is used if port is omitted.
640 self
._logger
.info('Connecting to MPD...')
642 if self
.is_connected():
643 self
._logger
.warning('Already connected.')
645 self
._socket
.connect_changed
.connect(lambda val
: self
._handle
_connected
() if val
else self
._handle
_disconnected
())
646 self
._socket
.connect_mpd(host
, port
)
647 self
._password
= password
649 def disconnect_mpd(self
):
653 self
._logger
.info('Disconnecting from MPD.')
654 if self
.is_connected():
655 self
._socket
.write_command('close')
657 def is_connected(self
):
659 Returns True if connected to MPD, False otherwise.
661 return self
._socket
.state() == QtNetwork
.QAbstractSocket
.ConnectedState
663 def playlist(self
, callback
):
665 Request current playlist from MPD. Callback will be called with an
666 iterator over Songs in current playlist as the argument.
668 self
._command
('playlistinfo', callback
= lambda data
: callback(self
._parse
_songs
(data
)))
669 def database(self
, callback
):
671 Request database information from MPD. Callback will be called with an
672 iterator over all Songs in the database as the argument.
674 self
._command
('listallinfo', callback
= lambda data
: callback(self
._parse
_songs
(data
)))
676 def find(self
, callback
, *args
):
678 Request a search on MPD. Callback will be called with an iterator over
679 all found songs. For allowed values of args, see MPD protocol documentation.
681 self
._command
('find', args
, callback
= lambda data
: callback(self
._parse
_songs
(data
)))
682 def find_sync(self
, *args
):
684 Search for songs on MPD synchronously. Returns an iterator over all
685 found songs. For allowed values of args, see MPD protocol documentation.
687 return self
._command
_sync
('find', args
, parse
= lambda data
: self
._parse
_songs
(data
))
688 def findadd(self
, *args
):
690 Request a search on MPD and add found songs to current playlist. Allowed values
691 of args are same as for find.
693 self
._command
('findadd', *args
)
695 def get_plist_song(self
, plid
):
697 Get a song with a given playlist id synchronously.
699 return self
._command
_sync
('playlistid', plid
, parse
= lambda data
: Song(list(self
._parse
_objects
(data
, []))[0]))
700 def set_volume(self
, volume
):
702 Set MPD volume level.
704 volume
= min(100, max(0, volume
))
705 self
._command
('setvol', volume
)
706 def repeat(self
, val
):
708 Enable/disable repeat.
710 val
= '1' if val
else '0'
711 self
._command
('repeat', val
)
712 def random(self
, val
):
714 Enable/disable random.
716 val
= '1' if val
else '0'
717 self
._command
('random', val
)
718 def consume(self
, val
):
720 Enable/disable consume.
722 val
= '1' if val
else '0'
723 self
._command
('consume', val
)
724 def single(self
, val
):
726 Enable/disable single.
728 val
= '1' if val
else '0'
729 self
._command
('single', val
)
730 def crossfade(self
, time
):
732 Set crossfade to specified time.
734 self
._command
('crossfade', val
)
736 def play(self
, id = None):
738 Start playback of song with a specified id. If no id is given, then
739 start on current song/beginning.
749 self
._command
('pause', 1)
752 Resume paused playback.
754 self
._command
('pause', 0)
757 Move on to next song.
759 self
._command
('next')
762 Move back to previous song.
764 self
._command
('previous')
769 self
._command
('stop')
770 def seek(self
, time
):
772 Seek to specified time in current song.
774 self
._command
('seekid', self
.status
['songid'], time
)
776 def delete(self
, ids
):
778 Delete songs with specified ids from playlist.
781 self
._command
('deleteid', id)
784 Clear current playlist.
786 self
._command
('clear')
787 def add(self
, paths
, pos
= -1):
789 Add specified songs to specified position in current playlist.
791 # start playback of the first added song if MPD is stopped
792 if self
.status
['state'] == 'stop':
793 cb
= lambda data
: [self
.play(sid
) for sid
in self
._parse
_list
(data
) ]
803 self
._command
(*args
, callback
= cb
)
809 def move(self
, src
, dst
):
811 Move a song with given src id to position dst.
813 self
._command
('moveid', src
, dst
)
817 ## connection functions ##
818 # these functions are called during connection process #
819 # XXX: maybe use a generator?
821 def _handle_connected(self
):
823 Called when a connection is established. Send a password and
824 start getting locally stored values.
826 self
._logger
.debug('Connection established.')
828 # check if protocol version is supported
829 v
= self
._socket
.version
830 if v
[0] != self
._sup
_ver
[0]:
831 self
._logger
.error('Server reported unsupported major protocol version %d, disconnecting.'%v
[0])
832 return self
.disconnect_mpd()
833 if v
[1] < self
._sup
_ver
[1]:
834 self
._logger
.warning('Server reported too low minor protocol version %d. Continuing, but things might break.'%v
[1])
837 self
._socket
.write_command('password', self
._password
)
839 self
._socket
.write_command('commands', callback
= self
._parse
_commands
)
841 def _parse_commands(self
, data
):
843 Receive a list of available commands and update
844 the other locally stored values.
846 self
._logger
.debug('Receiving command list.')
847 self
._commands
= list(self
._parse
_list
(data
))
849 if not 'listallinfo' in self
._commands
:
850 self
._logger
.error('Don\'t have MPD read permission, diconnecting.')
851 return self
.disconnect_mpd()
853 # update cached values
854 self
._command
('outputs', callback
= self
._parse
_outputs
)
855 self
._command
('tagtypes', callback
= self
._parse
_tagtypes
)
856 self
._command
('urlhandlers', callback
= self
._parse
_urlhandlers
)
858 def _parse_outputs(self
, data
):
860 Update a list of outputs.
862 self
._logger
.debug('Receiving outputs.')
864 for output
in self
._parse
_objects
(data
, ['outputid']):
865 self
.outputs
.append(AudioOutput(output
, lambda val
, outid
= output
['outputid']: self
._set
_output
(outid
, val
), self
))
867 def _parse_tagtypes(self
, data
):
869 Update a list of tag types.
871 self
._logger
.debug('Receiving tag types.')
872 self
.tagtypes
= list(self
._parse
_list
(data
)) + ['file']
873 def _parse_urlhandlers(self
, data
):
875 Update a list of URL handlers and finish connection.
877 self
._logger
.debug('Receiving URL handlers.')
878 self
.urlhandlers
= list(self
._parse
_list
(data
))
880 # done initializing data, finish connecting
881 return self
._finish
_connect
()
883 def _finish_connect(self
):
885 Called when connecting is completely done. Emit all signals.
887 self
._logger
.info('Successfully connected to MPD.')
889 self
._socket
.subsystems_changed
.connect(self
._mpd
_changed
)
890 self
.connect_changed
.emit(True)
894 def _handle_disconnected(self
):
896 Called when connection is closed. Clear all cached data and emit
897 corresponding signals.
899 self
._logger
.info('Disconnected from MPD.')
903 self
.urlhandlers
= []
906 self
.connect_changed
.emit(False)
908 ################################
911 def _mpd_changed(self
, subsystems
= None):
913 Called when MPD signals a change in some subsystems.
916 subsystems
= ['database', 'update', 'stored_playlist', 'playlist', 'output',
917 'player', 'mixer', 'options']
919 if ('player' in subsystems
or
920 'mixer' in subsystems
or
921 'options' in subsystems
):
922 self
._command
('status', callback
= self
._update
_status
)
923 if 'database' in subsystems
:
924 self
.db_updated
.emit()
925 if 'update' in subsystems
:
926 pass # just list for completeness
927 if 'stored_playlist' in subsystems
:
929 if 'playlist' in subsystems
:
930 self
.playlist_changed
.emit()
931 if 'output' in subsystems
:
932 self
._command
('outputs', callback
= self
._update
_outputs
)
934 def _update_outputs(self
, data
):
936 Update outputs states.
938 for output
in self
._parse
_objects
(data
, ['outputid']):
939 self
.outputs
[int(output
['outputid'])].update(output
)
941 def _update_status(self
, data
):
943 Called when something in status has changed. Check what was it and emit
944 corresponding signals.
948 self
.status
= MPDStatus(list(self
._parse
_objects
(data
, ''))[0])
950 self
.status
= MPDStatus()
952 if self
.status
['state'] == 'play':
957 if status
['state'] != self
.status
['state']:
958 self
.state_changed
.emit(self
.status
['state'])
959 if status
['time'][0] != self
.status
['time'][0]:
960 self
.time_changed
.emit(self
.status
['time'][0])
961 if status
['volume'] != self
.status
['volume']:
962 self
.volume_changed
.emit(self
.status
['volume'])
963 if status
['repeat'] != self
.status
['repeat']:
964 self
.repeat_changed
.emit(self
.status
['repeat'])
965 if status
['random'] != self
.status
['random']:
966 self
.random_changed
.emit(self
.status
['random'])
967 if status
['single'] != self
.status
['single']:
968 self
.single_changed
.emit(self
.status
['single'])
969 if status
['consume'] != self
.status
['consume']:
970 self
.consume_changed
.emit(self
.status
['consume'])
971 if status
['playlist'] != self
.status
['playlist']:
972 self
.playlist_changed
.emit()
973 if status
['songid'] != self
.status
['songid']:
974 self
._command
('currentsong', callback
= self
._update
_cur
_song
)
976 def _update_cur_song(self
, data
):
978 self
.cur_song
= Song(list(self
._parse
_objects
(data
, ''))[0])
980 self
.cur_song
= Song()
981 self
.song_changed
.emit(self
.cur_song
)
983 def _command(self
, *cmd
, **kwargs
):
985 Send specified command to MPD asynchronously. kwargs must contain
986 a callable 'callback' if the caller want to read a response. Otherwise
987 any reponse from MPD is silently discarded.
989 if not self
.is_connected():
990 self
._logger
.debug('Not connected -- not running command: %s'%cmd
[0])
991 if 'callback' in kwargs
:
992 kwargs
['callback']([])
993 elif not cmd
[0] in self
._commands
:
994 self
._logger
.error('Command %s not allowed.'%cmd
[0])
995 if 'callback' in kwargs
:
996 kwargs
['callback']([])
998 self
._socket
.write_command(*cmd
, **kwargs
)
999 def _command_sync(self
, *cmd
, **kwargs
):
1001 Send specified command to MPD synchronously. kwargs must contain
1002 a callable 'parse' used for parsing the reponse.
1004 parse
= kwargs
['parse']
1006 if not self
.is_connected():
1007 self
._logger
.debug('Not connected -- not running command: %s'%cmd
[0])
1009 elif not cmd
[0] in self
._commands
:
1010 self
._logger
.error('Command %s not allowed.'%cmd
[0])
1013 return parse(self
._socket
.write_command_sync(*cmd
))
1015 def _set_output(self
, out_id
, val
):
1017 Enable/disable speciffied output. Called only by AudioOutput.
1019 cmd
= 'enableoutput' if val
else 'disableoutput'
1020 self
._command
(cmd
, out_id
)
1022 def _update_timer(self
):
1023 self
.status
['time'][0] += 1
1024 self
.time_changed
.emit(self
.status
['time'][0])
1026 ## MPD output parsing functions ##
1028 def _parse_list(self
, data
):
1030 Parse a list of 'id_we_dont_care_about: useful_data'.
1033 parts
= line
.partition(': ')
1035 self
._logger
.error('Malformed line: %s.'%line
)
1039 def _parse_objects(self
, data
, delimiters
):
1041 Parse a list of object separated by specified delimiters.
1045 parts
= line
.partition(': ')
1047 self
._logger
.error('Malformed line: %s.'%line
)
1050 if parts
[0] in delimiters
and cur
:
1055 cur
[parts
[0]] += ',' + parts
[2]
1057 cur
[parts
[0]] = parts
[2]
1061 def _parse_songs(self
, data
):
1063 Parse a list of songs -- output of playlistinfo/listallinfo.
1065 for song
in self
._parse
_objects
(data
, ['file', 'directory']):