Refactor powerline.segments.common.players to use multiple functions

Also adds documentation. Still have to update top-level themes.

Fixes #445
This commit is contained in:
ZyX 2014-10-12 14:56:11 +04:00
parent 0eeab7fda0
commit a37f90921f
2 changed files with 249 additions and 60 deletions

View File

@ -27,6 +27,12 @@ def import_function(function_type, name, data, context, echoerr, module):
problem='module {0} is deprecated'.format(module), problem='module {0} is deprecated'.format(module),
problem_mark=module.mark) problem_mark=module.mark)
if module == 'powerline.segments.common.players' and name == 'now_playing':
echoerr(context='Warning while checking segments (key {key})'.format(key=context.key),
context_mark=name.mark,
problem='function {0}.{1} is deprecated: use {0}.{{player_name}} instead'.format(module, name),
problem_mark=module.mark)
with WithPath(data['import_paths']): with WithPath(data['import_paths']):
try: try:
func = getattr(__import__(str(module), fromlist=[str(name)]), str(name)) func = getattr(__import__(str(module), fromlist=[str(name)]), str(name))

View File

@ -5,7 +5,7 @@ import sys
from powerline.lib.shell import asrun, run_cmd from powerline.lib.shell import asrun, run_cmd
from powerline.lib.unicode import out_u from powerline.lib.unicode import out_u
from powerline.segments import Segment from powerline.segments import Segment, with_docstring
STATE_SYMBOLS = { STATE_SYMBOLS = {
@ -16,9 +16,25 @@ STATE_SYMBOLS = {
} }
class NowPlayingSegment(Segment): def _convert_state(state):
def __call__(self, player='mpd', format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs): '''Guess player state'''
player_func = getattr(self, 'player_{0}'.format(player)) state = state.lower()
if 'play' in state:
return 'play'
if 'pause' in state:
return 'pause'
if 'stop' in state:
return 'stop'
return 'fallback'
def _convert_seconds(seconds):
'''Convert seconds to minutes:seconds format'''
return '{0:.0f}:{1:02.0f}'.format(*divmod(float(seconds), 60))
class PlayerSegment(Segment):
def __call__(self, format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs):
stats = { stats = {
'state': 'fallback', 'state': 'fallback',
'album': None, 'album': None,
@ -27,28 +43,72 @@ class NowPlayingSegment(Segment):
'elapsed': None, 'elapsed': None,
'total': None, 'total': None,
} }
func_stats = player_func(**kwargs) func_stats = self.get_player_status(**kwargs)
if not func_stats: if not func_stats:
return None return None
stats.update(func_stats) stats.update(func_stats)
stats['state_symbol'] = state_symbols.get(stats['state']) stats['state_symbol'] = state_symbols.get(stats['state'])
return format.format(**stats) return format.format(**stats)
@staticmethod def argspecobjs(self):
def _convert_state(state): for ret in super(PlayerSegment, self).argspecobjs():
state = state.lower() yield ret
if 'play' in state: yield 'get_player_status', self.get_player_status
return 'play'
if 'pause' in state:
return 'pause'
if 'stop' in state:
return 'stop'
@staticmethod def omitted_args(self, name, method):
def _convert_seconds(seconds): return (0,)
return '{0:.0f}:{1:02.0f}'.format(*divmod(float(seconds), 60))
def player_cmus(self, pl):
_common_args = '''
This player segment should be added like this:
.. code-block:: json
{{
"function": "powerline.segments.common.players.{0}",
"name": "player"
}}
(with additional ``"args": {{}}`` if needed).
:param str format:
Format used for displaying data from player. Should be a str.format-like
string with the following keyword parameters:
+------------+-------------------------------------------------------------+
|Parameter |Description |
+============+=============================================================+
|state_symbol|Symbol displayed for play/pause/stop states. There is also |
| |fallback state used in case function failed to get player |
| |state. For this state symbol is by default empty. All |
| |symbols are defined in ``state_symbols`` argument. |
+------------+-------------------------------------------------------------+
|album |Album that is currently played. |
+------------+-------------------------------------------------------------+
|artist |Artist whose song is currently played |
+------------+-------------------------------------------------------------+
|title |Currently played composition. |
+------------+-------------------------------------------------------------+
|elapsed |Composition duration in format M:SS (minutes:seconds). |
+------------+-------------------------------------------------------------+
|total |Composition length in format M:SS. |
+------------+-------------------------------------------------------------+
:param dict state_symbols:
Symbols used for displaying state. Must contain all of the following keys:
======== ========================================================
Key Description
======== ========================================================
play Displayed when player is playing.
pause Displayed when player is paused.
stop Displayed when player is not playing anything.
fallback Displayed if state is not one of the above or not known.
======== ========================================================
'''
class CmusPlayerSegment(PlayerSegment):
def get_player_status(self, pl):
'''Return cmus player information. '''Return cmus player information.
cmus-remote -Q returns data with multi-level information i.e. cmus-remote -Q returns data with multi-level information i.e.
@ -75,17 +135,28 @@ class NowPlayingSegment(Segment):
now_playing = dict(((token[0] if token[0] not in ignore_levels else token[1], now_playing = dict(((token[0] if token[0] not in ignore_levels else token[1],
(' '.join(token[1:]) if token[0] not in ignore_levels else (' '.join(token[1:]) if token[0] not in ignore_levels else
' '.join(token[2:]))) for token in [line.split(' ') for line in now_playing_str.split('\n')[:-1]])) ' '.join(token[2:]))) for token in [line.split(' ') for line in now_playing_str.split('\n')[:-1]]))
state = self._convert_state(now_playing.get('status')) state = _convert_state(now_playing.get('status'))
return { return {
'state': state, 'state': state,
'album': now_playing.get('album'), 'album': now_playing.get('album'),
'artist': now_playing.get('artist'), 'artist': now_playing.get('artist'),
'title': now_playing.get('title'), 'title': now_playing.get('title'),
'elapsed': self._convert_seconds(now_playing.get('position', 0)), 'elapsed': _convert_seconds(now_playing.get('position', 0)),
'total': self._convert_seconds(now_playing.get('duration', 0)), 'total': _convert_seconds(now_playing.get('duration', 0)),
} }
def player_mpd(self, pl, host='localhost', port=6600):
cmus = with_docstring(CmusPlayerSegment(),
('''Return CMUS player information
Requires cmus-remote command be acessible from $PATH.
{0}
''').format(_common_args.format('cmus')))
class MpdPlayerSegment(PlayerSegment):
def get_player_status(self, pl, host='localhost', port=6600):
try: try:
import mpd import mpd
except ImportError: except ImportError:
@ -113,16 +184,33 @@ class NowPlayingSegment(Segment):
'album': now_playing.get('album'), 'album': now_playing.get('album'),
'artist': now_playing.get('artist'), 'artist': now_playing.get('artist'),
'title': now_playing.get('title'), 'title': now_playing.get('title'),
'elapsed': self._convert_seconds(now_playing.get('elapsed', 0)), 'elapsed': _convert_seconds(now_playing.get('elapsed', 0)),
'total': self._convert_seconds(now_playing.get('time', 0)), 'total': _convert_seconds(now_playing.get('time', 0)),
} }
def player_dbus(self, player_name, bus_name, player_path, iface_prop, iface_player):
try: mpd = with_docstring(MpdPlayerSegment(),
('''Return Music Player Daemon information
Requires mpc command to be acessible from $PATH or ``mpd`` Python module.
{0}
:param str host:
Host on which mpd runs.
:param int port:
Port which should be connected to.
''').format(_common_args.format('mpd')))
try:
import dbus import dbus
except ImportError: except ImportError:
self.exception('Could not add {0} segment: requires dbus module', player_name) def _get_dbus_player_status(pl, player_name, **kwargs):
pl.error('Could not add {0} segment: requires dbus module', player_name)
return return
else:
def _get_dbus_player_status(pl, bus_name, player_path, iface_prop,
iface_player, player_name='player'):
bus = dbus.SessionBus() bus = dbus.SessionBus()
try: try:
player = bus.get_object(bus_name, player_path) player = bus.get_object(bus_name, player_path)
@ -136,7 +224,7 @@ class NowPlayingSegment(Segment):
album = out_u(info.get('xesam:album')) album = out_u(info.get('xesam:album'))
title = out_u(info.get('xesam:title')) title = out_u(info.get('xesam:title'))
artist = info.get('xesam:artist') artist = info.get('xesam:artist')
state = self._convert_state(status) state = _convert_state(status)
if artist: if artist:
artist = out_u(artist[0]) artist = out_u(artist[0])
return { return {
@ -144,11 +232,38 @@ class NowPlayingSegment(Segment):
'album': album, 'album': album,
'artist': artist, 'artist': artist,
'title': title, 'title': title,
'total': self._convert_seconds(info.get('mpris:length') / 1e6), 'total': _convert_seconds(info.get('mpris:length') / 1e6),
} }
def player_spotify_dbus(self, pl):
return self.player_dbus( class DbusPlayerSegment(PlayerSegment):
get_player_status = staticmethod(_get_dbus_player_status)
dbus_player = with_docstring(DbusPlayerSegment(),
('''Return generic dbus player state
Requires ``dbus`` python module. Only for players that support specific protocol
(e.g. like :py:func:`spotify` and :py:func:`clementine`).
{0}
:param str player_name:
Player name. Used in error messages only.
:param str bus_name:
Dbus bus name.
:param str player_path:
Path to the player on the given bus.
:param str iface_prop:
Interface properties name for use with dbus.Interface.
:param str iface_player:
Player name.
''').format(_common_args.format('dbus_player')))
class SpotifyDbusPlayerSegment(PlayerSegment):
def get_player_status(self, pl):
return _get_dbus_player_status(
pl=pl,
player_name='Spotify', player_name='Spotify',
bus_name='com.spotify.qt', bus_name='com.spotify.qt',
player_path='/', player_path='/',
@ -156,16 +271,18 @@ class NowPlayingSegment(Segment):
iface_player='org.freedesktop.MediaPlayer2', iface_player='org.freedesktop.MediaPlayer2',
) )
def player_clementine(self, pl):
return self.player_dbus(
player_name='Clementine',
bus_name='org.mpris.MediaPlayer2.clementine',
player_path='/org/mpris/MediaPlayer2',
iface_prop='org.freedesktop.DBus.Properties',
iface_player='org.mpris.MediaPlayer2.Player',
)
def player_spotify_apple_script(self, pl): spotify_dbus = with_docstring(SpotifyDbusPlayerSegment(),
('''Return spotify player information
Requires ``dbus`` python module.
{0}
''').format(_common_args.format('spotify_dbus')))
class SpotifyAppleScriptPlayerSegment(PlayerSegment):
def get_player_status(self, pl):
status_delimiter = '-~`/=' status_delimiter = '-~`/='
ascript = ''' ascript = '''
tell application "System Events" tell application "System Events"
@ -196,7 +313,7 @@ class NowPlayingSegment(Segment):
return None return None
spotify_status = spotify.split(status_delimiter) spotify_status = spotify.split(status_delimiter)
state = self._convert_state(spotify_status[0]) state = _convert_state(spotify_status[0])
if state == 'stop': if state == 'stop':
return None return None
return { return {
@ -204,21 +321,58 @@ class NowPlayingSegment(Segment):
'album': spotify_status[1], 'album': spotify_status[1],
'artist': spotify_status[2], 'artist': spotify_status[2],
'title': spotify_status[3], 'title': spotify_status[3],
'total': self._convert_seconds(int(spotify_status[4])) 'total': _convert_seconds(int(spotify_status[4]))
} }
try:
__import__('dbus')
except ImportError:
if sys.platform.startswith('darwin'):
player_spotify = player_spotify_apple_script
else:
player_spotify = player_spotify_dbus
else:
player_spotify = player_spotify_dbus
def player_rhythmbox(self, pl): spotify_apple_script = with_docstring(SpotifyAppleScriptPlayerSegment(),
now_playing = run_cmd(pl, ['rhythmbox-client', '--no-start', '--no-present', '--print-playing-format', '%at\n%aa\n%tt\n%te\n%td']) ('''Return spotify player information
Requires ``osascript`` available in $PATH.
{0}
''').format(_common_args.format('spotify_apple_script')))
if 'dbus' in globals() or not sys.platform.startswith('darwin'):
spotify = spotify_dbus
_old_name = 'spotify_dbus'
else:
spotify = spotify_apple_script
_old_name = 'spotify_apple_script'
spotify = with_docstring(spotify, spotify.__doc__.replace(_old_name, 'spotify'))
class ClementinePlayerSegment(PlayerSegment):
def get_player_status(self, pl):
return _get_dbus_player_status(
pl=pl,
player_name='Clementine',
bus_name='org.mpris.MediaPlayer2.clementine',
player_path='/org/mpris/MediaPlayer2',
iface_prop='org.freedesktop.DBus.Properties',
iface_player='org.mpris.MediaPlayer2.Player',
)
clementine = with_docstring(ClementinePlayerSegment(),
('''Return clementine player information
Requires ``dbus`` python module.
{0}
''').format(_common_args.format('clementine')))
class RhythmboxPlayerSegment(PlayerSegment):
def get_player_status(self, pl):
now_playing = run_cmd(pl, [
'rhythmbox-client',
'--no-start', '--no-present',
'--print-playing-format', '%at\n%aa\n%tt\n%te\n%td'
])
if not now_playing: if not now_playing:
return return
now_playing = now_playing.split('\n') now_playing = now_playing.split('\n')
@ -230,7 +384,18 @@ class NowPlayingSegment(Segment):
'total': now_playing[4], 'total': now_playing[4],
} }
def player_rdio(self, pl):
rhythmbox = with_docstring(RhythmboxPlayerSegment(),
('''Return rhythmbox player information
Requires ``rhythmbox-client`` available in $PATH.
{0}
''').format(_common_args.format('rhythmbox')))
class RDIOPlayerSegment(PlayerSegment):
def get_player_status(self, pl):
status_delimiter = '-~`/=' status_delimiter = '-~`/='
ascript = ''' ascript = '''
tell application "System Events" tell application "System Events"
@ -255,9 +420,9 @@ class NowPlayingSegment(Segment):
now_playing = now_playing.split('\n') now_playing = now_playing.split('\n')
if len(now_playing) != 6: if len(now_playing) != 6:
return return
state = self._convert_state(now_playing[5]) state = _convert_state(now_playing[5])
total = self._convert_seconds(now_playing[4]) total = _convert_seconds(now_playing[4])
elapsed = self._convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100) elapsed = _convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100)
return { return {
'title': now_playing[0], 'title': now_playing[0],
'artist': now_playing[1], 'artist': now_playing[1],
@ -267,4 +432,22 @@ class NowPlayingSegment(Segment):
'state': state, 'state': state,
'state_symbol': self.STATE_SYMBOLS.get(state) 'state_symbol': self.STATE_SYMBOLS.get(state)
} }
rdio = with_docstring(RDIOPlayerSegment(),
('''Return rdio player information
Requires ``osascript`` available in $PATH.
{0}
''').format(_common_args.format('rdio')))
class NowPlayingSegment(Segment):
def __call__(self, player='mpd', **kwargs):
player_segment = globals()[player]
assert(isinstance(player_segment, PlayerSegment))
return player_segment(**kwargs)
now_playing = NowPlayingSegment() now_playing = NowPlayingSegment()