Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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('')