#!/usr/bin/env python # MIT License # # Copyright (c) 2018 Andreas Backx # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Spotify DBus listener""" import os import socket import logging from concurrent.futures import ThreadPoolExecutor import click import dbus # type: ignore import dbus.mainloop.glib # type: ignore from dbus.mainloop.glib import DBusGMainLoop from gi.repository import GLib # type: ignore #from spotipy import SpotifyException #from spotipy.oauth2 import SpotifyClientCredentials INACTIVE_COLOR = '%{F#6E6E6E}' ACTIVE_COLOR = '%{F#CECECE}' DEFAULT_COLOR = '%{F-}' SERVER_ADDRESS = '/tmp/spotifycl-socket' class Spotify: """Spotify DBus Listener""" # pylint: disable=too-many-instance-attributes SPOTIFY_BUS = 'org.mpris.MediaPlayer2.spotify' SPOTIFYD_BUS = 'org.mpris.MediaPlayer2.spotifyd' SPOTIFY_OBJECT_PATH = '/org/mpris/MediaPlayer2' PLAYER_INTERFACE = 'org.mpris.MediaPlayer2.Player' PROPERTIES_INTERFACE = 'org.freedesktop.DBus.Properties' SAVE_REMOVE = b'save' logging.basicConfig(filename="/tmp/spotifycl.log", level=logging.DEBUG) logger = logging.getLogger("spotifycl") def __init__(self): DBusGMainLoop(set_as_default=True) self.session_bus = dbus.SessionBus() self.last_output = '' self.empty_output = True # Last shown metadata self.last_title = None # Whether the current song is added to the library self.saved_track = False # Whether to ignore the update self.ignore = False # DBus session object self.freedesktop = None self.spotify = None def monitor(self): """ Monitor """ self.logger.info("monitor") self.setup_properties_changed() self.freedesktop = self.session_bus.get_object( "org.freedesktop.DBus", "/org/freedesktop/DBus" ) self.freedesktop.connect_to_signal( "NameOwnerChanged", self.on_name_owner_changed, arg0=self.SPOTIFYD_BUS ) executor = ThreadPoolExecutor(max_workers=2) executor.submit(self._start_glib_loop) executor.submit(self._start_server) @staticmethod def _start_glib_loop(): """ Start Glib loop """ loop = GLib.MainLoop() loop.run() @staticmethod def _start_server(): try: os.unlink(SERVER_ADDRESS) except OSError: if os.path.exists(SERVER_ADDRESS): raise sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(SERVER_ADDRESS) sock.listen(5) @property def metadata_status(self): """ Get song status """ spotify_properties = dbus.Interface( self.spotify, dbus_interface=Spotify.PROPERTIES_INTERFACE ) metadata = spotify_properties.Get( Spotify.PLAYER_INTERFACE, 'Metadata' ) playback_status = spotify_properties.Get( Spotify.PLAYER_INTERFACE, 'PlaybackStatus' ) return metadata, playback_status def output(self, line): """ Output for polybar """ if not line: self.empty_output = True if line != self.last_output: print(line, flush=True) self.last_output = line def setup_spotify(self): """ Setup spotify DBus session object """ self.logger.info("setup spotify") try: self.spotify = self.session_bus.get_object( Spotify.SPOTIFY_BUS, Spotify.SPOTIFY_OBJECT_PATH ) except dbus.DBusException as error: self.logger.warning("DbusException: %s", error) self.spotify = self.session_bus.get_object( Spotify.SPOTIFYD_BUS, Spotify.SPOTIFY_OBJECT_PATH) def setup_properties_changed(self): """ Setup propertise changed """ try: self.setup_spotify() self.spotify.connect_to_signal( 'PropertiesChanged', self.on_properties_changed ) if self.empty_output: metadata, playback_status = self.metadata_status self.output_playback_status( data={ 'Metadata': metadata, 'PlaybackStatus': playback_status, } ) except dbus.DBusException: self.logger.warning("Exception") self.output('') def output_playback_status(self, data, retry=False): """ output current song """ if self.ignore: return metadata = data['Metadata'] artists = metadata['xesam:artist'] artist = artists[0] if artists else None if not artist: self.output('') return title = metadata['xesam:title'] playback_status = data['PlaybackStatus'] same_song = title == self.last_title color = ACTIVE_COLOR if playback_status == 'Playing' else INACTIVE_COLOR # divider = '+' if same_song and self.saved_track else '-' self.output(f'{color}{artist} - {title}{DEFAULT_COLOR}') if not same_song: self.last_title = title def on_properties_changed(self, interface, data, *args, **kwargs): """On name properties changed event""" del interface, args, kwargs self.output_playback_status(data) def on_name_owner_changed(self, name, old_owner, new_owner): """On name owner changed event""" del old_owner if name == self.SPOTIFY_BUS: if new_owner: # Spotify was opened. self.setup_properties_changed() else: # Spotify was closed. self.spotify = None self.output('') @click.group() def cli(): """Script for listening to Spotify over dbus and adding tracks to your library.""" @cli.command() def status(): """Follow the status of the currently playing song on Spotify.""" spotify = Spotify() spotify.monitor() if __name__ == '__main__': cli()