Merge branch 'enhance-now_playing' into develop

This commit is contained in:
ZyX 2014-10-12 16:54:30 +04:00
commit 5635eea233
12 changed files with 374 additions and 67 deletions

View File

@ -29,6 +29,24 @@ object it should receive the following arguments:
And also any other argument(s) specified by user in :ref:`args key
<config-themes-seg-args>` (no additional arguments by default).
.. note::
For powerline-lint to work properly the following things may be needed:
#. If your segment is a :py:class:`powerline.segments.Segment` and used
arguments are scattered over multiple methods
:py:meth:`powerline.segments.Segment.argspecobjs` should be overridden in
subclass to tell powerline-lint which objects should be inspected for
arguments.
#. If your segment takes some arguments that are never listed, but accessed
via ``kwargs.get()`` or you cannot use previous function for whatever
reason :py:meth:`powerline.segments.Segment.additional_args` should be
overridden in subclass.
#. If you are expecting user to use one :ref:`name <config-themes-seg-name>`
for multiple segments which cannot be linked to the segment function
automatically by powerline-lint (e.g. because there are no instances of
the segments in question in the default configuration) you should use
:py:func:`powerline.lint.checks.register_common_name`.
Object representing segment may have the following attributes used by
powerline:

View File

@ -28,7 +28,7 @@
"battery_gradient": { "fg": "white_red", "bg": "gray0", "attr": [] },
"battery_full": { "fg": "red", "bg": "gray0", "attr": [] },
"battery_empty": { "fg": "white", "bg": "gray0", "attr": [] },
"now_playing": { "fg": "gray10", "bg": "black", "attr": [] },
"player": { "fg": "gray10", "bg": "black", "attr": [] },
"user": { "fg": "white", "bg": "darkblue", "attr": ["bold"] },
"superuser": { "fg": "white", "bg": "brightred", "attr": ["bold"] },
"branch": { "fg": "gray9", "bg": "gray2", "attr": [] },

View File

@ -20,6 +20,16 @@
"ellipsis": "..."
}
},
"player": {
"args": {
"state_symbols": {
"fallback": "",
"play": ">",
"pause": "~",
"stop": "X"
}
}
},
"line_current_symbol": {
"contents": "LN "

View File

@ -23,6 +23,16 @@
"line_current_symbol": {
"contents": " "
},
"player": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"time": {
"before": "⌚ "

View File

@ -19,6 +19,16 @@
"ellipsis": "⋯"
}
},
"player": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"line_current_symbol": {
"contents": "␤ "

View File

@ -23,6 +23,16 @@
"line_current_symbol": {
"contents": "␤ "
},
"player": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"time": {
"before": ""

View File

@ -24,6 +24,16 @@
"line_current_symbol": {
"contents": "␤"
},
"player": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"time": {
"before": ""

View File

@ -16,7 +16,7 @@ if sys.platform.startswith('win32'):
Popen = partial(Popen, creationflags=0x08000000)
def run_cmd(pl, cmd, stdin=None):
def run_cmd(pl, cmd, stdin=None, strip=True):
'''Run command and return its stdout, stripped
If running command fails returns None and logs failure to ``pl`` argument.
@ -27,6 +27,8 @@ def run_cmd(pl, cmd, stdin=None):
Command which will be run.
:param str stdin:
String passed to command. May be None.
:param bool strip:
True if the result should be stripped.
'''
try:
p = Popen(cmd, shell=False, stdout=PIPE, stdin=PIPE)
@ -36,7 +38,7 @@ def run_cmd(pl, cmd, stdin=None):
else:
stdout, err = p.communicate(stdin)
stdout = stdout.decode(get_preferred_input_encoding())
return stdout.strip()
return stdout.strip() if strip else stdout
def asrun(pl, ascript):

View File

@ -19,7 +19,7 @@ from powerline.lint.checks import (check_matcher_func, check_ext, check_config,
check_segment_module, check_exinclude_function, type_keys,
check_segment_function, check_args, get_one_segment_function,
check_highlight_groups, check_highlight_group, check_full_segment_data,
get_all_possible_functions, check_segment_data_key)
get_all_possible_functions, check_segment_data_key, register_common_name)
from powerline.lint.spec import Spec
from powerline.lint.context import Context
@ -289,6 +289,10 @@ theme_spec = common_theme_spec().update(
)
def register_common_names():
register_common_name('player', 'powerline.segments.common.players', '_player')
def check(paths=None, debug=False, echoerr=echoerr, require_ext=None):
'''Check configuration sanity
@ -308,6 +312,7 @@ def check(paths=None, debug=False, echoerr=echoerr, require_ext=None):
``False`` if user configuration seems to be completely sane and ``True``
if some problems were found.
'''
register_common_names()
search_paths = paths or get_config_paths()
find_config_files = generate_config_finder(lambda: search_paths)

View File

@ -5,6 +5,8 @@ import os
import re
import logging
from collections import defaultdict
from powerline.lib.threaded import ThreadedSegment
from powerline.lib.unicode import unicode
from powerline.lint.markedjson.markedvalue import MarkedUnicode
@ -673,6 +675,16 @@ def get_one_segment_function(data, context, echoerr):
yield func
common_names = defaultdict(set)
def register_common_name(name, cmodule, cname):
s = cmodule + '.' + cname
cmodule_mark = Mark('<common name definition>', 1, 1, s, 1)
cname_mark = Mark('<common name definition>', 1, len(cmodule) + 1, s, len(cmodule) + 1)
common_names[name].add((MarkedUnicode(cmodule, cmodule_mark), MarkedUnicode(cname, cname_mark)))
def get_all_possible_functions(data, context, echoerr):
name = context[-2][0]
module, name = name.rpartition('.')[::2]
@ -681,6 +693,11 @@ def get_all_possible_functions(data, context, echoerr):
if func:
yield func
else:
if name in common_names:
for cmodule, cname in common_names[name]:
cfunc = import_segment(cname, data, context, echoerr, module=MarkedUnicode(cmodule, None))
if cfunc:
yield cfunc
for ext, theme_config in list_themes(data, context):
for segments in theme_config.get('segments', {}).values():
for segment in segments:

View File

@ -27,6 +27,12 @@ def import_function(function_type, name, data, context, echoerr, module):
problem='module {0} is deprecated'.format(module),
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']):
try:
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.unicode import out_u
from powerline.segments import Segment
from powerline.segments import Segment, with_docstring
STATE_SYMBOLS = {
@ -16,9 +16,25 @@ STATE_SYMBOLS = {
}
class NowPlayingSegment(Segment):
def __call__(self, player='mpd', format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs):
player_func = getattr(self, 'player_{0}'.format(player))
def _convert_state(state):
'''Guess player state'''
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 = {
'state': 'fallback',
'album': None,
@ -27,28 +43,83 @@ class NowPlayingSegment(Segment):
'elapsed': None,
'total': None,
}
func_stats = player_func(**kwargs)
func_stats = self.get_player_status(**kwargs)
if not func_stats:
return None
stats.update(func_stats)
stats['state_symbol'] = state_symbols.get(stats['state'])
return format.format(**stats)
return [{
'contents': format.format(**stats),
'highlight_group': ['now_playing', 'player_' + (stats['state'] or 'fallback'), 'player'],
}]
@staticmethod
def _convert_state(state):
state = state.lower()
if 'play' in state:
return 'play'
if 'pause' in state:
return 'pause'
if 'stop' in state:
return 'stop'
def get_player_status(self, pl):
pass
@staticmethod
def _convert_seconds(seconds):
return '{0:.0f}:{1:02.0f}'.format(*divmod(float(seconds), 60))
def argspecobjs(self):
for ret in super(PlayerSegment, self).argspecobjs():
yield ret
yield 'get_player_status', self.get_player_status
def player_cmus(self, pl):
def omitted_args(self, name, method):
return (0,)
_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).
Highlight groups used: ``player_fallback`` or ``player``, ``player_play`` or ``player``, ``player_pause`` or ``player``, ``player_stop`` or ``player``.
: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.
======== ========================================================
'''
_player = with_docstring(PlayerSegment(), _common_args.format('_player'))
class CmusPlayerSegment(PlayerSegment):
def get_player_status(self, pl):
'''Return cmus player information.
cmus-remote -Q returns data with multi-level information i.e.
@ -75,21 +146,37 @@ class NowPlayingSegment(Segment):
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[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 {
'state': state,
'album': now_playing.get('album'),
'artist': now_playing.get('artist'),
'title': now_playing.get('title'),
'elapsed': self._convert_seconds(now_playing.get('position', 0)),
'total': self._convert_seconds(now_playing.get('duration', 0)),
'elapsed': _convert_seconds(now_playing.get('position', 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:
import mpd
except ImportError:
now_playing = run_cmd(pl, ['mpc', 'current', '-f', '%album%\n%artist%\n%title%\n%time%', '-h', str(host), '-p', str(port)])
now_playing = run_cmd(pl, [
'mpc', 'current',
'-f', '%album%\n%artist%\n%title%\n%time%',
'-h', str(host),
'-p', str(port)
], strip=False)
if not now_playing:
return
now_playing = now_playing.split('\n')
@ -113,16 +200,33 @@ class NowPlayingSegment(Segment):
'album': now_playing.get('album'),
'artist': now_playing.get('artist'),
'title': now_playing.get('title'),
'elapsed': self._convert_seconds(now_playing.get('elapsed', 0)),
'total': self._convert_seconds(now_playing.get('time', 0)),
'elapsed': _convert_seconds(now_playing.get('elapsed', 0)),
'total': _convert_seconds(now_playing.get('time', 0)),
}
def player_dbus(self, player_name, bus_name, player_path, iface_prop, iface_player):
try:
import dbus
except ImportError:
self.exception('Could not add {0} segment: requires dbus module', player_name)
return
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
except ImportError:
def _get_dbus_player_status(pl, player_name, **kwargs):
pl.error('Could not add {0} segment: requires dbus module', player_name)
return
else:
def _get_dbus_player_status(pl, bus_name, player_path, iface_prop,
iface_player, player_name='player'):
bus = dbus.SessionBus()
try:
player = bus.get_object(bus_name, player_path)
@ -136,7 +240,7 @@ class NowPlayingSegment(Segment):
album = out_u(info.get('xesam:album'))
title = out_u(info.get('xesam:title'))
artist = info.get('xesam:artist')
state = self._convert_state(status)
state = _convert_state(status)
if artist:
artist = out_u(artist[0])
return {
@ -144,11 +248,38 @@ class NowPlayingSegment(Segment):
'album': album,
'artist': artist,
'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',
bus_name='com.spotify.qt',
player_path='/',
@ -156,16 +287,18 @@ class NowPlayingSegment(Segment):
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 = '-~`/='
ascript = '''
tell application "System Events"
@ -196,7 +329,7 @@ class NowPlayingSegment(Segment):
return None
spotify_status = spotify.split(status_delimiter)
state = self._convert_state(spotify_status[0])
state = _convert_state(spotify_status[0])
if state == 'stop':
return None
return {
@ -204,21 +337,58 @@ class NowPlayingSegment(Segment):
'album': spotify_status[1],
'artist': spotify_status[2],
'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):
now_playing = run_cmd(pl, ['rhythmbox-client', '--no-start', '--no-present', '--print-playing-format', '%at\n%aa\n%tt\n%te\n%td'])
spotify_apple_script = with_docstring(SpotifyAppleScriptPlayerSegment(),
('''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'
], strip=False)
if not now_playing:
return
now_playing = now_playing.split('\n')
@ -230,7 +400,18 @@ class NowPlayingSegment(Segment):
'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 = '-~`/='
ascript = '''
tell application "System Events"
@ -255,9 +436,9 @@ class NowPlayingSegment(Segment):
now_playing = now_playing.split('\n')
if len(now_playing) != 6:
return
state = self._convert_state(now_playing[5])
total = self._convert_seconds(now_playing[4])
elapsed = self._convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100)
state = _convert_state(now_playing[5])
total = _convert_seconds(now_playing[4])
elapsed = _convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100)
return {
'title': now_playing[0],
'artist': now_playing[1],
@ -265,6 +446,34 @@ class NowPlayingSegment(Segment):
'elapsed': elapsed,
'total': total,
'state': 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)
def argspecobjs(self):
for ret in super(NowPlayingSegment, self).argspecobjs():
yield ret
yield '__call__', PlayerSegment.__call__
for k, v in globals().items():
if isinstance(v, type) and issubclass(v, PlayerSegment) and v is not DbusPlayerSegment:
yield 'get_player_status', v.get_player_status
def omitted_args(self, name, method):
return (0,)
now_playing = NowPlayingSegment()