def populate_user(sp: spotipy.Spotify, s, user: models.User, song_ids: List[str]) -> int: candidate: models.Playlist = s.query(models.Playlist).filter_by( owner=user.username, candidate=True).first() pre_ids = set(get_song_ids(sp, candidate.playlist_id)) in_saveds = sp.current_user_saved_tracks_contains(song_ids) res: List[str] = [ song_ids[i] for i, in_saved in enumerate(in_saveds) if not in_saved and song_ids[i] not in pre_ids ] if res: sp.user_playlist_add_tracks(user.username, candidate.playlist_id, res) user.last_updated = int(time()) s.add(user) s.commit() return len(res)
class Spotifycl: SPOTIFY_BUS = 'org.mpris.MediaPlayer2.spotify' SPOTIFY_OBJECT_PATH = '/org/mpris/MediaPlayer2' PLAYER_INTERFACE = 'org.mpris.MediaPlayer2.Player' PROPERTIES_INTERFACE = 'org.freedesktop.DBus.Properties' SAVE_REMOVE = b'save' CLASS_PLAYING = 'playing' CLASS_PAUSED = 'paused' CLASS_SAVED = 'saved' PLAY = 'org.mpris.MediaPlayer2.Player.Play' PAUSE = 'org.mpris.MediaPlayer2.Player.Pause' PLAY_PAUSE = 'org.mpris.MediaPlayer2.Player.PlayPause' STOP = 'org.mpris.MediaPlayer2.Player.Stop' PREVIOUS = 'org.mpris.MediaPlayer2.Player.Previous' NEXT = 'org.mpris.MediaPlayer2.Player.Next' 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 self.setup_spotipy() self.spotify_dbus = None @classmethod def dbus_command(cls, command): subprocess.run( [ 'dbus-send', '--print-reply', f'--dest={cls.SPOTIFY_BUS}', cls.SPOTIFY_OBJECT_PATH, command, ], stdout=subprocess.DEVNULL ) def monitor(self): 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='org.mpris.MediaPlayer2.spotify' ) executor = ThreadPoolExecutor(max_workers=2) executor.submit(self._start_glib_loop) executor.submit(self._start_server) def status(self): self.connect_spotify_dbus() print(self.metadata_status[1]) def _start_glib_loop(self): loop = GLib.MainLoop() loop.run() def _start_server(self): 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) while True: connection, client_address = sock.accept() try: command = connection.recv(16) if command == Spotifycl.SAVE_REMOVE: self.save_remove() except Exception as e: print(e) finally: connection.close() def stop_server(self): self.server_loop.close() def send_to_server(self, command: bytes): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: sock.connect(server_address) except socket.error: raise try: sock.sendall(command) finally: sock.close() @property def metadata_status(self): spotify_properties = dbus.Interface( self.spotify_dbus, dbus_interface=Spotifycl.PROPERTIES_INTERFACE ) metadata = spotify_properties.Get( Spotifycl.PLAYER_INTERFACE, 'Metadata' ) playback_status = spotify_properties.Get( Spotifycl.PLAYER_INTERFACE, 'PlaybackStatus' ) return metadata, playback_status.lower() def setup_spotipy(self): username = os.environ.get('SPOTIFY_USERNAME') cache_path = os.path.join( os.environ.get('HOME', '~'), f'.spotipy-{username}' ) # If you get an error here, you have to install the git version of spotipy: # pip uninstall spotipy # pip install git+https://github.com/plamere/spotipy.git@master#spotipy auth = util.prompt_for_user_token( username=username, scope='user-library-read,user-library-modify', cache_path=cache_path ) self.spotify = Spotify(auth=auth) def save_remove(self, retry=False): try: metadata, playback_status = self.metadata_status trackid = metadata['mpris:trackid'] self.ignore = True remove = self.saved_track self.saved_track = not self.saved_track try: if remove: self.spotify.current_user_saved_tracks_delete(tracks=[trackid]) self.output('Removed from library!') else: self.spotify.current_user_saved_tracks_add(tracks=[trackid]) self.output('Saved to library!') except SpotifyException: if not retry: # Refresh access token self.setup_spotipy() self.save_remove(retry=True) return else: raise time.sleep(2) self.ignore = False metadata, playback_status = self.metadata_status self.output_playback_status( data={ 'Metadata': metadata, 'PlaybackStatus': playback_status, } ) except dbus.DBusException: self.output('Could not connect to spotify.') def output(self, text, tooltip=None, css_class=None): text = '' if text is None else text output = { 'text': html.escape(text), } if tooltip: output['tooltip'] = tooltip if css_class and isinstance(css_class, (str, list)): output['class'] = css_class if not text: self.empty_output = True if output != self.last_output: serialized = json.dumps(output) print(serialized, flush=True) self.last_output = output def connect_spotify_dbus(self): if self.spotify_dbus is None: self.spotify_dbus = self.session_bus.get_object( Spotifycl.SPOTIFY_BUS, Spotifycl.SPOTIFY_OBJECT_PATH ) def setup_properties_changed(self): try: self.connect_spotify_dbus() self.spotify_dbus.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.output('') def _song_info(self, data): """Return song info from passed data. Args: data (dict): Metadata and PlaybackStatus. Returns: tuple: arist, title, playing, album, trackid """ metadata = data['Metadata'] artists = metadata['xesam:artist'] artist = artists[0] if artists else None title = metadata['xesam:title'] playback_status = data['PlaybackStatus'].lower() # playback_status can be 'Playing', 'Paused', or 'Stopped' # 'Stopped' is not used here. playing = playback_status == 'playing' album = metadata['xesam:album'] trackid = metadata['mpris:trackid'] return artist, title, playing, album, trackid def output_playback_status(self, data, retry=False): if self.ignore: return artist, title, playing, album, trackid = self._song_info(data) if not artist: self.output('') return same_song = title == self.last_title saved = same_song and self.saved_track divider = '+' if saved else '-' css_class = [ Spotifycl.CLASS_PLAYING if playing else Spotifycl.CLASS_PAUSED, ] if saved: css_class.append(Spotifycl.CLASS_SAVED) output = { 'text': f'{artist} {divider} {title}', 'css_class': css_class, } if album: output['tooltip'] = album self.output(**output) if not same_song: self.last_title = title try: self.update_saved_track(trackid=trackid) except SpotifyException: # Refresh access token self.setup_spotipy() self.update_saved_track(trackid=trackid) if self.saved_track: output['text'] = f'{artist} + {title}' self.output(**output) def update_saved_track(self, trackid: str): self.saved_track = self.spotify.current_user_saved_tracks_contains( tracks=[trackid] )[0] def on_properties_changed(self, interface, data, *args, **kwargs): self.output_playback_status(data) def on_name_owner_changed(self, name, old_owner, new_owner): if name == Spotifycl.SPOTIFY_BUS: if new_owner: # Spotify was opened. self.setup_properties_changed() else: # Spotify was closed. self.spotify_dbus = None self.output('')