608 lines
17 KiB
Python
608 lines
17 KiB
Python
# vim:fileencoding=utf-8:noet
|
||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||
|
||
import sys
|
||
|
||
from powerline.lib.shell import asrun, run_cmd
|
||
from powerline.lib.unicode import out_u
|
||
from powerline.segments import Segment, with_docstring
|
||
|
||
|
||
STATE_SYMBOLS = {
|
||
'fallback': '',
|
||
'play': '>',
|
||
'pause': '~',
|
||
'stop': 'X',
|
||
}
|
||
|
||
|
||
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,
|
||
'artist': None,
|
||
'title': None,
|
||
'elapsed': None,
|
||
'total': None,
|
||
}
|
||
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 [{
|
||
'contents': format.format(**stats),
|
||
'highlight_groups': ['player_' + (stats['state'] or 'fallback'), 'player'],
|
||
}]
|
||
|
||
def get_player_status(self, pl):
|
||
pass
|
||
|
||
def argspecobjs(self):
|
||
for ret in super(PlayerSegment, self).argspecobjs():
|
||
yield ret
|
||
yield 'get_player_status', self.get_player_status
|
||
|
||
def omitted_args(self, name, method):
|
||
return ()
|
||
|
||
|
||
_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.
|
||
status playing
|
||
file <file_name>
|
||
tag artist <artist_name>
|
||
tag title <track_title>
|
||
tag ..
|
||
tag n
|
||
set continue <true|false>
|
||
set repeat <true|false>
|
||
set ..
|
||
set n
|
||
|
||
For the information we are looking for we don’t really care if we’re on
|
||
the tag level or the set level. The dictionary comprehension in this
|
||
method takes anything in ignore_levels and brings the key inside that
|
||
to the first level of the dictionary.
|
||
'''
|
||
now_playing_str = run_cmd(pl, ['cmus-remote', '-Q'])
|
||
if not now_playing_str:
|
||
return
|
||
ignore_levels = ('tag', 'set',)
|
||
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 = _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': _convert_seconds(now_playing.get('position', 0)),
|
||
'total': _convert_seconds(now_playing.get('duration', 0)),
|
||
}
|
||
|
||
|
||
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', password=None, port=6600):
|
||
try:
|
||
import mpd
|
||
except ImportError:
|
||
if password:
|
||
host = password + '@' + host
|
||
now_playing = run_cmd(pl, [
|
||
'mpc', 'current',
|
||
'-f', '%album%\n%artist%\n%title%\n%time%',
|
||
'-h', host,
|
||
'-p', str(port)
|
||
], strip=False)
|
||
if not now_playing:
|
||
return
|
||
now_playing = now_playing.split('\n')
|
||
return {
|
||
'album': now_playing[0],
|
||
'artist': now_playing[1],
|
||
'title': now_playing[2],
|
||
'total': now_playing[3],
|
||
}
|
||
else:
|
||
try:
|
||
client = mpd.MPDClient(use_unicode=True)
|
||
except TypeError:
|
||
# python-mpd 1.x does not support use_unicode
|
||
client = mpd.MPDClient()
|
||
client.connect(host, port)
|
||
if password:
|
||
client.password(password)
|
||
now_playing = client.currentsong()
|
||
if not now_playing:
|
||
return
|
||
status = client.status()
|
||
client.close()
|
||
client.disconnect()
|
||
return {
|
||
'state': status.get('state'),
|
||
'album': now_playing.get('album'),
|
||
'artist': now_playing.get('artist'),
|
||
'title': now_playing.get('title'),
|
||
'elapsed': _convert_seconds(status.get('elapsed', 0)),
|
||
'total': _convert_seconds(now_playing.get('time', 0)),
|
||
}
|
||
|
||
|
||
mpd = with_docstring(MpdPlayerSegment(),
|
||
('''Return Music Player Daemon information
|
||
|
||
Requires ``mpd`` Python module (e.g. |python-mpd2|_ or |python-mpd|_ Python
|
||
package) or alternatively the ``mpc`` command to be acessible from $PATH.
|
||
|
||
.. |python-mpd| replace:: ``python-mpd``
|
||
.. _python-mpd: https://pypi.python.org/pypi/python-mpd
|
||
|
||
.. |python-mpd2| replace:: ``python-mpd2``
|
||
.. _python-mpd2: https://pypi.python.org/pypi/python-mpd2
|
||
|
||
{0}
|
||
:param str host:
|
||
Host on which mpd runs.
|
||
:param str password:
|
||
Password used for connecting to daemon.
|
||
: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)
|
||
iface = dbus.Interface(player, iface_prop)
|
||
info = iface.Get(iface_player, 'Metadata')
|
||
status = iface.Get(iface_player, 'PlaybackStatus')
|
||
except dbus.exceptions.DBusException:
|
||
return
|
||
if not info:
|
||
return
|
||
|
||
try:
|
||
elapsed = iface.Get(iface_player, 'Position')
|
||
except dbus.exceptions.DBusException:
|
||
pl.warning('Missing player elapsed time')
|
||
elapsed = None
|
||
else:
|
||
elapsed = _convert_seconds(elapsed / 1e6)
|
||
album = info.get('xesam:album')
|
||
title = info.get('xesam:title')
|
||
artist = info.get('xesam:artist')
|
||
state = _convert_state(status)
|
||
if album:
|
||
album = out_u(album)
|
||
if title:
|
||
title = out_u(title)
|
||
if artist:
|
||
artist = out_u(artist[0])
|
||
return {
|
||
'state': state,
|
||
'album': album,
|
||
'artist': artist,
|
||
'title': title,
|
||
'elapsed': elapsed,
|
||
'total': _convert_seconds(info.get('mpris:length') / 1e6),
|
||
}
|
||
|
||
|
||
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):
|
||
player_status = _get_dbus_player_status(
|
||
pl=pl,
|
||
player_name='Spotify',
|
||
bus_name='org.mpris.MediaPlayer2.spotify',
|
||
player_path='/org/mpris/MediaPlayer2',
|
||
iface_prop='org.freedesktop.DBus.Properties',
|
||
iface_player='org.mpris.MediaPlayer2.Player',
|
||
)
|
||
if player_status is not None:
|
||
return player_status
|
||
# Fallback for legacy spotify client with different DBus protocol
|
||
return _get_dbus_player_status(
|
||
pl=pl,
|
||
player_name='Spotify',
|
||
bus_name='com.spotify.qt',
|
||
player_path='/',
|
||
iface_prop='org.freedesktop.DBus.Properties',
|
||
iface_player='org.freedesktop.MediaPlayer2',
|
||
)
|
||
|
||
|
||
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"
|
||
set process_list to (name of every process)
|
||
end tell
|
||
|
||
if process_list contains "Spotify" then
|
||
tell application "Spotify"
|
||
if player state is playing or player state is paused then
|
||
set track_name to name of current track
|
||
set artist_name to artist of current track
|
||
set album_name to album of current track
|
||
set track_length to duration of current track
|
||
set now_playing to "" & player state & "{0}" & album_name & "{0}" & artist_name & "{0}" & track_name & "{0}" & track_length & "{0}" & player position
|
||
return now_playing
|
||
else
|
||
return player state
|
||
end if
|
||
|
||
end tell
|
||
else
|
||
return "stopped"
|
||
end if
|
||
'''.format(status_delimiter)
|
||
|
||
spotify = asrun(pl, ascript)
|
||
if not asrun:
|
||
return None
|
||
|
||
spotify_status = spotify.split(status_delimiter)
|
||
state = _convert_state(spotify_status[0])
|
||
if state == 'stop':
|
||
return None
|
||
return {
|
||
'state': state,
|
||
'album': spotify_status[1],
|
||
'artist': spotify_status[2],
|
||
'title': spotify_status[3],
|
||
'total': _convert_seconds(int(spotify_status[4])/1000),
|
||
'elapsed': _convert_seconds(spotify_status[5]),
|
||
}
|
||
|
||
|
||
spotify_apple_script = with_docstring(SpotifyAppleScriptPlayerSegment(),
|
||
('''Return spotify player information
|
||
|
||
Requires ``osascript`` available in $PATH.
|
||
|
||
{0}
|
||
''').format(_common_args.format('spotify_apple_script')))
|
||
|
||
|
||
if 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')
|
||
return {
|
||
'album': now_playing[0],
|
||
'artist': now_playing[1],
|
||
'title': now_playing[2],
|
||
'elapsed': now_playing[3],
|
||
'total': now_playing[4],
|
||
}
|
||
|
||
|
||
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"
|
||
set rdio_active to the count(every process whose name is "Rdio")
|
||
if rdio_active is 0 then
|
||
return
|
||
end if
|
||
end tell
|
||
tell application "Rdio"
|
||
set rdio_name to the name of the current track
|
||
set rdio_artist to the artist of the current track
|
||
set rdio_album to the album of the current track
|
||
set rdio_duration to the duration of the current track
|
||
set rdio_state to the player state
|
||
set rdio_elapsed to the player position
|
||
return rdio_name & "{0}" & rdio_artist & "{0}" & rdio_album & "{0}" & rdio_elapsed & "{0}" & rdio_duration & "{0}" & rdio_state
|
||
end tell
|
||
'''.format(status_delimiter)
|
||
now_playing = asrun(pl, ascript)
|
||
if not now_playing:
|
||
return
|
||
now_playing = now_playing.split(status_delimiter)
|
||
if len(now_playing) != 6:
|
||
return
|
||
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],
|
||
'album': now_playing[2],
|
||
'elapsed': elapsed,
|
||
'total': total,
|
||
'state': state,
|
||
}
|
||
|
||
|
||
rdio = with_docstring(RDIOPlayerSegment(),
|
||
('''Return rdio player information
|
||
|
||
Requires ``osascript`` available in $PATH.
|
||
|
||
{0}
|
||
''').format(_common_args.format('rdio')))
|
||
|
||
|
||
class ITunesPlayerSegment(PlayerSegment):
|
||
def get_player_status(self, pl):
|
||
status_delimiter = '-~`/='
|
||
ascript = '''
|
||
tell application "System Events"
|
||
set process_list to (name of every process)
|
||
end tell
|
||
|
||
if process_list contains "iTunes" then
|
||
tell application "iTunes"
|
||
if player state is playing then
|
||
set t_title to name of current track
|
||
set t_artist to artist of current track
|
||
set t_album to album of current track
|
||
set t_duration to duration of current track
|
||
set t_elapsed to player position
|
||
set t_state to player state
|
||
return t_title & "{0}" & t_artist & "{0}" & t_album & "{0}" & t_elapsed & "{0}" & t_duration & "{0}" & t_state
|
||
end if
|
||
end tell
|
||
end if
|
||
'''.format(status_delimiter)
|
||
now_playing = asrun(pl, ascript)
|
||
if not now_playing:
|
||
return
|
||
now_playing = now_playing.split(status_delimiter)
|
||
if len(now_playing) != 6:
|
||
return
|
||
title, artist, album = now_playing[0], now_playing[1], now_playing[2]
|
||
state = _convert_state(now_playing[5])
|
||
total = _convert_seconds(now_playing[4])
|
||
elapsed = _convert_seconds(now_playing[3])
|
||
return {
|
||
'title': title,
|
||
'artist': artist,
|
||
'album': album,
|
||
'total': total,
|
||
'elapsed': elapsed,
|
||
'state': state
|
||
}
|
||
|
||
|
||
itunes = with_docstring(ITunesPlayerSegment(),
|
||
('''Return iTunes now playing information
|
||
|
||
Requires ``osascript``.
|
||
|
||
{0}
|
||
''').format(_common_args.format('itunes')))
|
||
|
||
|
||
class MocPlayerSegment(PlayerSegment):
|
||
def get_player_status(self, pl):
|
||
'''Return Music On Console (mocp) player information.
|
||
|
||
``mocp -i`` returns current information i.e.
|
||
|
||
.. code-block::
|
||
|
||
File: filename.format
|
||
Title: full title
|
||
Artist: artist name
|
||
SongTitle: song title
|
||
Album: album name
|
||
TotalTime: 00:00
|
||
TimeLeft: 00:00
|
||
TotalSec: 000
|
||
CurrentTime: 00:00
|
||
CurrentSec: 000
|
||
Bitrate: 000kbps
|
||
AvgBitrate: 000kbps
|
||
Rate: 00kHz
|
||
|
||
For the information we are looking for we don’t really care if we have
|
||
extra-timing information or bit rate level. The dictionary comprehension
|
||
in this method takes anything in ignore_info and brings the key inside
|
||
that to the right info of the dictionary.
|
||
'''
|
||
now_playing_str = run_cmd(pl, ['mocp', '-i'])
|
||
if not now_playing_str:
|
||
return
|
||
|
||
now_playing = dict((
|
||
line.split(': ', 1)
|
||
for line in now_playing_str.split('\n')[:-1]
|
||
))
|
||
state = _convert_state(now_playing.get('State', 'stop'))
|
||
return {
|
||
'state': state,
|
||
'album': now_playing.get('Album', ''),
|
||
'artist': now_playing.get('Artist', ''),
|
||
'title': now_playing.get('SongTitle', ''),
|
||
'elapsed': _convert_seconds(now_playing.get('CurrentSec', 0)),
|
||
'total': _convert_seconds(now_playing.get('TotalSec', 0)),
|
||
}
|
||
|
||
|
||
mocp = with_docstring(MocPlayerSegment(),
|
||
('''Return MOC (Music On Console) player information
|
||
|
||
Requires version >= 2.3.0 and ``mocp`` executable in ``$PATH``.
|
||
|
||
{0}
|
||
''').format(_common_args.format('mocp')))
|
||
|