summaryrefslogtreecommitdiffstats
path: root/powerline/segments/common/players.py
blob: f43db0c12c6dfac946ea44b27abcbfd32176f09b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)

import sys
import re

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'''
	if isinstance(seconds, str):
	        seconds = seconds.replace(",",".")
	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 accessible 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',
				'-h', host,
				'-p', str(port)
			], strip=False)
			album = run_cmd(pl, [
				'mpc', 'current',
				'-f', '%album%',
				'-h', host,
				'-p', str(port)
			])
			if not now_playing or now_playing.count("\n") != 3:
				return
			now_playing = re.match(
				r"(.*) - (.*)\n\[([a-z]+)\] +[#0-9\/]+ +([0-9\:]+)\/([0-9\:]+)",
				now_playing
			)
			return {
				'state': _convert_state(now_playing[3]),
				'album': album,
				'artist': now_playing[1],
				'title': now_playing[2],
				'elapsed': now_playing[4],
				'total': now_playing[5]
			}
		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 accessible 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=None,
				iface_prop='org.freedesktop.DBus.Properties',
				iface_player='org.mpris.MediaPlayer2.Player',
				player_path='/org/mpris/MediaPlayer2',
				player_name='player'):
		bus = dbus.SessionBus()

		if bus_name is None:
			for service in bus.list_names():
				if re.match('org.mpris.MediaPlayer2.', service):
					bus_name = service
					break

		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])

		length = info.get('mpris:length')
		# avoid parsing `None` length values, that would
		# raise an error otherwise
		parsed_length = length and _convert_seconds(length / 1e6)

		return {
			'state': state,
			'album': album,
			'artist': artist,
			'title': title,
			'elapsed': elapsed,
			'total': parsed_length,
		}


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')))