Advertisement
Guest User

Untitled

a guest
Feb 23rd, 2019
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.12 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import sys
  4. import dbus
  5. import os
  6. from operator import itemgetter
  7. import argparse
  8. import re
  9. from urllib.parse import unquote
  10. import time
  11. from dbus.mainloop.glib import DBusGMainLoop
  12. from gi.repository import GLib
  13. DBusGMainLoop(set_as_default=True)
  14.  
  15.  
  16. FORMAT_STRING = '{icon} {artist} - {title}'
  17. FORMAT_REGEX = re.compile(r'(\{:(?P<tag>.*?)(:(?P<format>[wt])(?P<formatlen>\d+))?:(?P<text>.*?):\})', re.I)
  18. FORMAT_TAG_REGEX = re.compile(r'(?P<format>[wt])(?P<formatlen>\d+)')
  19. SAFE_TAG_REGEX = re.compile(r'[{}]')
  20.  
  21. class PlayerManager:
  22.     def __init__(self, blacklist = [], connect = True):
  23.         self.blacklist = blacklist
  24.         self._connect = connect
  25.         self._session_bus = dbus.SessionBus()
  26.         self.players = {}
  27.  
  28.         self.print_queue = []
  29.         self.connected = False
  30.         self.player_states = {}
  31.        
  32.         self.refreshPlayerList()
  33.  
  34.         if self._connect:
  35.             self.connect()
  36.             loop = GLib.MainLoop()
  37.             try:
  38.                 loop.run()
  39.             except KeyboardInterrupt:
  40.                 print("interrupt received, stopping…")
  41.    
  42.     def connect(self):
  43.         self._session_bus.add_signal_receiver(self.onOwnerChangedName, 'NameOwnerChanged')
  44.    
  45.     def onOwnerChangedName(self, bus_name, old_owner, new_owner):
  46.         if self.busNameIsAPlayer(bus_name):
  47.             if new_owner and not old_owner:
  48.                 self.addPlayer(bus_name, new_owner)
  49.             elif old_owner and not new_owner:
  50.                 self.removePlayer(old_owner)
  51.             else:
  52.                 self.changePlayerOwner(bus_name, old_owner, new_owner)
  53.  
  54.     def busNameIsAPlayer(self, bus_name):
  55.         return bus_name.startswith('org.mpris.MediaPlayer2') and bus_name.split('.')[-1] not in self.blacklist
  56.  
  57.     def refreshPlayerList(self):
  58.         player_bus_names = [ bus_name for bus_name in self._session_bus.list_names() if self.busNameIsAPlayer(bus_name) ]
  59.         for player_bus_name in player_bus_names:
  60.             self.addPlayer(player_bus_name)
  61.         if self.connected != True:
  62.             self.connected = True
  63.             self.printQueue()
  64.  
  65.     def addPlayer(self, bus_name, owner = None):
  66.         player = Player(self._session_bus, bus_name, owner = owner, connect = self._connect, _print = self.print)
  67.         self.players[player.owner] = player
  68.        
  69.     def removePlayer(self, owner):
  70.         self.players[owner].disconnect()
  71.         del self.players[owner]
  72.         if len(self.players) == 0:
  73.             _printFlush(ICON_NONE)
  74.  
  75.     def changePlayerOwner(self, bus_name, old_owner, new_owner):
  76.         player = Player(self._session_bus, bus_name, owner = new_owner, connect = self._connect, _print = self.print)
  77.         self.players[new_owner] = player
  78.         del self.players[old_owner]
  79.    
  80.     # Get a list of player owners sorted by current status and age
  81.     def getSortedPlayerOwnerList(self):
  82.         players = [
  83.             {
  84.                 'number': int(owner.split('.')[-1]),
  85.                 'status': 2 if player.status == 'playing' else 1 if player.status == 'paused' else 0,
  86.                 'owner': owner
  87.             }
  88.             for owner, player in self.players.items()
  89.         ]
  90.         return [ info['owner'] for info in reversed(sorted(players, key=itemgetter('status', 'number'))) ]
  91.  
  92.     # Get latest player that's currently playing
  93.     def getCurrentPlayer(self):
  94.         playing_players = [
  95.             player_owner for player_owner in self.getSortedPlayerOwnerList()
  96.             if
  97.                 self.players[player_owner].status == 'playing' or
  98.                 self.players[player_owner].status == 'paused'
  99.         ]
  100.         return self.players[playing_players[0]] if playing_players else None
  101.  
  102.     def print(self, status, player):
  103.         self.player_states[player.bus_name] = status
  104.  
  105.         if self.connected:
  106.             current_player = self.getCurrentPlayer()
  107.             if current_player != None:
  108.                 _printFlush(self.player_states[current_player.bus_name])
  109.             else:
  110.                 _printFlush(ICON_STOPPED)
  111.         else:
  112.             self.print_queue.append([status, player])
  113.    
  114.     def printQueue(self):
  115.         for args in self.print_queue:
  116.             self.print(args[0], args[1])
  117.         self.print_queue.clear()
  118.  
  119.  
  120. class Player:
  121.     def __init__(self, session_bus, bus_name, owner = None, connect = True, _print = None):
  122.         self._session_bus = session_bus
  123.         self.bus_name = bus_name
  124.         self._disconnecting = False
  125.         self.__print = _print
  126.  
  127.         self.metadata = {
  128.             'artist' : '',
  129.             'album'  : '',
  130.             'title'  : '',
  131.             'track'  : 0
  132.         }
  133.         self._metadata = None
  134.         self.status = 'stopped'
  135.         self.icon = ICON_NONE
  136.         self.icon_reversed = ICON_PLAYING
  137.         if owner is not None:
  138.             self.owner = owner
  139.         else:
  140.             self.owner = self._session_bus.get_name_owner(bus_name)
  141.         self._obj = self._session_bus.get_object(self.bus_name, '/org/mpris/MediaPlayer2')
  142.         self._properties_interface = dbus.Interface(self._obj, dbus_interface='org.freedesktop.DBus.Properties')
  143.         self._introspect_interface = dbus.Interface(self._obj, dbus_interface='org.freedesktop.DBus.Introspectable')
  144.         self._media_interface      = dbus.Interface(self._obj, dbus_interface='org.mpris.MediaPlayer2')
  145.         self._player_interface     = dbus.Interface(self._obj, dbus_interface='org.mpris.MediaPlayer2.Player')
  146.         self._introspect = self._introspect_interface.get_dbus_method('Introspect', dbus_interface=None)
  147.         self._getProperty = self._properties_interface.get_dbus_method('Get', dbus_interface=None)
  148.         self._playerPlay      = self._player_interface.get_dbus_method('Play', dbus_interface=None)
  149.         self._playerPause     = self._player_interface.get_dbus_method('Pause', dbus_interface=None)
  150.         self._playerPlayPause = self._player_interface.get_dbus_method('PlayPause', dbus_interface=None)
  151.         self._playerStop      = self._player_interface.get_dbus_method('Stop', dbus_interface=None)
  152.         self._playerPrevious  = self._player_interface.get_dbus_method('Previous', dbus_interface=None)
  153.         self._playerNext      = self._player_interface.get_dbus_method('Next', dbus_interface=None)
  154.         self._playerRaise     = self._media_interface.get_dbus_method('Raise', dbus_interface=None)
  155.         self._signals = {}
  156.  
  157.         self.refreshStatus()
  158.         self.refreshMetadata()
  159.  
  160.         if connect:
  161.             self.printStatus()
  162.             self.connect()
  163.    
  164.     def play(self):
  165.         self._playerPlay()
  166.     def pause(self):
  167.         self._playerPause()
  168.     def playpause(self):
  169.         self._playerPlayPause()
  170.     def stop(self):
  171.         self._playerStop()
  172.     def previous(self):
  173.         self._playerPrevious()
  174.     def next(self):
  175.         self._playerNext()
  176.     def raisePlayer(self):
  177.         self._playerRaise()
  178.        
  179.     def connect(self):
  180.         if self._disconnecting is not True:
  181.             introspect_xml = self._introspect(self.bus_name, '/')
  182.             if 'TrackMetadataChanged' in introspect_xml:
  183.                 self._signals['track_metadata_changed'] = self._session_bus.add_signal_receiver(self.onMetadataChanged, 'TrackMetadataChanged', self.bus_name)
  184.             self._signals['properties_changed'] = self._properties_interface.connect_to_signal('PropertiesChanged', self.onPropertiesChanged)
  185.    
  186.     def disconnect(self):
  187.         self._disconnecting = True
  188.         for signal_name, signal_handler in list(self._signals.items()):
  189.             signal_handler.remove()
  190.             del self._signals[signal_name]
  191.    
  192.     def refreshStatus(self):
  193.         # Some clients (VLC) will momentarily create a new player before removing it again
  194.         # so we can't be sure the interface still exists
  195.         try:
  196.             self.status = str(self._getProperty('org.mpris.MediaPlayer2.Player', 'PlaybackStatus')).lower()
  197.             self.updateIcon()
  198.         except dbus.exceptions.DBusException:
  199.             self.disconnect()
  200.  
  201.     def refreshMetadata(self):
  202.         # Some clients (VLC) will momentarily create a new player before removing it again
  203.         # so we can't be sure the interface still exists
  204.         try:
  205.             self._metadata = self._getProperty('org.mpris.MediaPlayer2.Player', 'Metadata')
  206.             self._parseMetadata()
  207.         except dbus.exceptions.DBusException:
  208.             self.disconnect()
  209.    
  210.     def updateIcon(self):
  211.         self.icon = (
  212.             ICON_PLAYING if self.status == 'playing' else
  213.             ICON_PAUSED if self.status == 'paused' else
  214.             ICON_STOPPED if self.status == 'stopped' else
  215.             ICON_NONE
  216.         )
  217.         self.icon_reversed = (
  218.             ICON_PAUSED if self.status == 'playing' else
  219.             ICON_PLAYING
  220.         )
  221.    
  222.     def _print(self, status):
  223.         self.__print(status, self)
  224.  
  225.     def _parseMetadata(self):
  226.         if self._metadata != None:
  227.             artist = _getProperty(self._metadata, 'xesam:artist', [''])
  228.             if len(artist):
  229.                 self.metadata['artist'] = re.sub(SAFE_TAG_REGEX, """\1\1""", artist[0])
  230.             else:
  231.                 self.metadata['artist'] = ''
  232.             self.metadata['album']  = re.sub(SAFE_TAG_REGEX, """\1\1""", _getProperty(self._metadata, 'xesam:album', ''))
  233.             self.metadata['title']  = re.sub(SAFE_TAG_REGEX, """\1\1""", _getProperty(self._metadata, 'xesam:title', ''))
  234.             self.metadata['track']  = _getProperty(self._metadata, 'xesam:trackNumber', '')
  235.             length = str(_getProperty(self._metadata, 'xesam:length', ''))
  236.             if not len(length):
  237.                 length = str(_getProperty(self._metadata, 'mpris:length', ''))
  238.             if len(length):
  239.                 self.metadata['length'] = int(length)
  240.             else:
  241.                 self.metadata['length'] = 0
  242.             self.metadata['genre']    = _getProperty(self._metadata, 'xesam:genre', '')
  243.             self.metadata['disc']     = _getProperty(self._metadata, 'xesam:discNumber', '')
  244.             self.metadata['date']     = re.sub(SAFE_TAG_REGEX, """\1\1""", _getProperty(self._metadata, 'xesam:contentCreated', ''))
  245.             self.metadata['year']     = re.sub(SAFE_TAG_REGEX, """\1\1""", self.metadata['date'][0:4])
  246.             self.metadata['url']      = _getProperty(self._metadata, 'xesam:url', '')
  247.             self.metadata['filename'] = os.path.basename(self.metadata['url'])
  248.             cover = _getProperty(self._metadata, 'xesam:artUrl', '')
  249.             if not len(cover):
  250.                 cover = _getProperty(self._metadata, 'mpris:artUrl', '')
  251.             if len(cover):
  252.                 self.metadata['cover'] = re.sub(SAFE_TAG_REGEX, """\1\1""", cover)
  253.             else:
  254.                 self.metadata['cover'] = ''
  255.  
  256.             self.metadata['duration'] = _getDuration(self.metadata['length'])
  257.    
  258.     def onMetadataChanged(self, track_id, metadata):
  259.         self.refreshMetadata()
  260.         self.printStatus()
  261.    
  262.     def onPropertiesChanged(self, interface, properties, signature):
  263.         updated = False
  264.         if dbus.String('Metadata') in properties:
  265.             _metadata = properties[dbus.String('Metadata')]
  266.             if _metadata != self._metadata:
  267.                 self._metadata = _metadata
  268.                 self._parseMetadata()
  269.                 updated = True
  270.         if dbus.String('PlaybackStatus') in properties:
  271.             status = str(properties[dbus.String('PlaybackStatus')]).lower()
  272.             if status != self.status:
  273.                 self.status = status
  274.                 self.updateIcon()
  275.                 updated = True
  276.        
  277.         if updated:
  278.             self.printStatus()
  279.  
  280.     def _statusReplace(self, match, metadata):
  281.         tag = match.group('tag')
  282.         format = match.group('format')
  283.         formatlen = match.group('formatlen')
  284.         text = match.group('text')
  285.         tag_found = False
  286.         reversed_tag = False
  287.        
  288.         if tag.startswith('-'):
  289.             tag = tag[1:]
  290.             reversed_tag = True
  291.        
  292.         if format is None:
  293.             tag_is_format_match = re.match(FORMAT_TAG_REGEX, tag)
  294.             if tag_is_format_match:
  295.                 format = tag_is_format_match.group('format')
  296.                 formatlen = tag_is_format_match.group('formatlen')
  297.                 tag_found = True
  298.         if format is not None:
  299.             text = text.format_map(CleanSafeDict(**metadata))
  300.             if format == 'w':
  301.                 formatlen = int(formatlen)
  302.                 text = text[:formatlen]
  303.             elif format == 't':
  304.                 formatlen = int(formatlen)
  305.                 if len(text) > formatlen:
  306.                     text = text[:max(formatlen - len(TRUNCATE_STRING), 0)] + TRUNCATE_STRING
  307.         if tag_found is False and tag in metadata and len(metadata[tag]):
  308.             tag_found = True
  309.  
  310.         if reversed_tag:
  311.             tag_found = not tag_found
  312.  
  313.         if tag_found:
  314.             return text
  315.         else:
  316.             return ''
  317.  
  318.     def printStatus(self):
  319.         if self.status in [ 'playing', 'paused' ]:
  320.             metadata = { **self.metadata, 'icon': self.icon, 'icon-reversed': self.icon_reversed }
  321.             # replace metadata tags in text
  322.             text = re.sub(FORMAT_REGEX, lambda match: self._statusReplace(match, metadata), FORMAT_STRING)
  323.             # restore polybar tag formatting and replace any remaining metadata tags after that
  324.             try:
  325.                 text = re.sub(r'􏿿p􏿿(.*?)􏿿p􏿿(.*?)􏿿p􏿿(.*?)􏿿p􏿿', r'%{\1}\2%{\3}', text.format_map(CleanSafeDict(**metadata)))
  326.             except:
  327.                 print("Invalid format string")
  328.             self._print(text)
  329.         else:
  330.             self._print(ICON_STOPPED)
  331.  
  332.  
  333. def _dbusValueToPython(value):
  334.     if isinstance(value, dbus.Dictionary):
  335.         return {_dbusValueToPython(key): _dbusValueToPython(value) for key, value in value.items()}
  336.     elif isinstance(value, dbus.Array):
  337.         return [ _dbusValueToPython(item) for item in value ]
  338.     elif isinstance(value, dbus.Boolean):
  339.         return int(value) == 1
  340.     elif (
  341.         isinstance(value, dbus.Byte) or
  342.         isinstance(value, dbus.Int16) or
  343.         isinstance(value, dbus.UInt16) or
  344.         isinstance(value, dbus.Int32) or
  345.         isinstance(value, dbus.UInt32) or
  346.         isinstance(value, dbus.Int64) or
  347.         isinstance(value, dbus.UInt64)
  348.     ):
  349.         return int(value)
  350.     elif isinstance(value, dbus.Double):
  351.         return float(value)
  352.     elif (
  353.         isinstance(value, dbus.ObjectPath) or
  354.         isinstance(value, dbus.Signature) or
  355.         isinstance(value, dbus.String)
  356.     ):
  357.         return unquote(str(value))
  358.  
  359. def _getProperty(properties, property, default = None):
  360.     value = default
  361.     if not isinstance(property, dbus.String):
  362.         property = dbus.String(property)
  363.     if property in properties:
  364.         value = properties[property]
  365.         return _dbusValueToPython(value)
  366.     else:
  367.         return value
  368.  
  369. def _getDuration(t: int):
  370.         seconds = t / 1000000
  371.         return time.strftime("%M:%S", time.gmtime(seconds))
  372.  
  373.  
  374. class CleanSafeDict(dict):
  375.     def __missing__(self, key):
  376.         return '{{{}}}'.format(key)
  377.  
  378.  
  379. """
  380. Seems to assure print() actually prints when no terminal is connected
  381. """
  382.  
  383. _last_status = ''
  384. def _printFlush(status, **kwargs):
  385.     global _last_status
  386.     if status != _last_status:
  387.         print(status, **kwargs)
  388.         sys.stdout.flush()
  389.         _last_status = status
  390.  
  391.  
  392.  
  393. parser = argparse.ArgumentParser()
  394. parser.add_argument('command', help="send the given command to the active player",
  395.                     choices=[ 'play', 'pause', 'play-pause', 'stop', 'previous', 'next', 'status', 'list', 'current', 'metadata', 'raise' ],
  396.                     default=None,
  397.                     nargs='?')
  398. parser.add_argument('-b', '--blacklist', help="ignore a player by it's bus name. Can be be given multiple times (e.g. -b vlc -b audacious)",
  399.                     action='append',
  400.                     metavar="BUS_NAME",
  401.                     default=[])
  402. parser.add_argument('-f', '--format', default='{icon} {:artist:{artist} - :}{:title:{title}:}{:-title:{filename}:}')
  403. parser.add_argument('--truncate-text', default='…')
  404. parser.add_argument('--icon-playing', default='⏵')
  405. parser.add_argument('--icon-paused', default='⏸')
  406. parser.add_argument('--icon-stopped', default='⏹')
  407. parser.add_argument('--icon-none', default='')
  408. args = parser.parse_args()
  409.  
  410. FORMAT_STRING = re.sub(r'%\{(.*?)\}(.*?)%\{(.*?)\}', r'􏿿p􏿿\1􏿿p􏿿\2􏿿p􏿿\3􏿿p􏿿', args.format)
  411. TRUNCATE_STRING = args.truncate_text
  412. ICON_PLAYING = args.icon_playing
  413. ICON_PAUSED = args.icon_paused
  414. ICON_STOPPED = args.icon_stopped
  415. ICON_NONE = args.icon_none
  416.  
  417. if args.command is None:
  418.     PlayerManager(blacklist = args.blacklist)
  419. else:
  420.     player_manager = PlayerManager(blacklist = args.blacklist, connect = False)
  421.     current_player = player_manager.getCurrentPlayer()
  422.     if args.command == 'play' and current_player:
  423.         current_player.play()
  424.     elif args.command == 'pause' and current_player:
  425.         current_player.pause()
  426.     elif args.command == 'play-pause' and current_player:
  427.         current_player.playpause()
  428.     elif args.command == 'stop' and current_player:
  429.         current_player.stop()
  430.     elif args.command == 'previous' and current_player:
  431.         current_player.previous()
  432.     elif args.command == 'next' and current_player:
  433.         current_player.next()
  434.     elif args.command == 'status' and current_player:
  435.         current_player.printStatus()
  436.     elif args.command == 'list':
  437.         print("\n".join(sorted([
  438.             "{} : {}".format(player.bus_name.split('.')[-1], player.status)
  439.             for player in player_manager.players.values() ])))
  440.     elif args.command == 'current' and current_player:
  441.         print("{} : {}".format(current_player.bus_name.split('.')[-1], current_player.status))
  442.     elif args.command == 'metadata' and current_player:
  443.         print(_dbusValueToPython(current_player._metadata))
  444.     elif args.command == 'raise' and current_player:
  445.         current_player.raisePlayer()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement