Beispiel #1
0
class Player(GObject.GObject):
    """Main Player object

    Contains the logic of playing a song with Music.
    """

    __gsignals__ = {
        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ())
    }

    state = GObject.Property(type=int, default=Playback.STOPPED)
    duration = GObject.Property(type=float, default=-1.)

    def __init__(self, application):
        """Initialize the player

        :param Application application: Application object
        """
        super().__init__()

        self._app = application
        # In the case of gapless playback, both 'about-to-finish'
        # and 'eos' can occur during the same stream. 'about-to-finish'
        # already sets self._playlist to the next song, so doing it
        # again on eos would skip a song.
        # TODO: Improve playlist handling so this hack is no longer
        # needed.
        self._gapless_set = False
        self._log = application.props.log
        self._playlist = PlayerPlaylist(self._app)

        self._playlist_model = self._app.props.coremodel.props.playlist_sort
        self._playlist_model.connect(
            "items-changed", self._on_playlist_model_items_changed)

        self._settings = application.props.settings
        self._settings.connect(
            'changed::repeat', self._on_repeat_setting_changed)

        self._repeat = self._settings.get_enum('repeat')
        self.bind_property(
            'repeat-mode', self._playlist, 'repeat-mode',
            GObject.BindingFlags.SYNC_CREATE)

        self._new_clock = True

        self._gst_player = GstPlayer(application)
        self._gst_player.connect("about-to-finish", self._on_about_to_finish)
        self._gst_player.connect('clock-tick', self._on_clock_tick)
        self._gst_player.connect('eos', self._on_eos)
        self._gst_player.connect("error", self._on_error)
        self._gst_player.connect('seek-finished', self._on_seek_finished)
        self._gst_player.connect("stream-start", self._on_stream_start)
        self._gst_player.bind_property(
            'duration', self, 'duration', GObject.BindingFlags.SYNC_CREATE)
        self._gst_player.bind_property(
            'state', self, 'state', GObject.BindingFlags.SYNC_CREATE)

        self._lastfm = application.props.lastfm_scrobbler

    @GObject.Property(
        type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def has_next(self):
        """Test if the playlist has a next song.

        :returns: True if the current song is not the last one.
        :rtype: bool
        """
        return self._playlist.has_next()

    @GObject.Property(
        type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def has_previous(self):
        """Test if the playlist has a previous song.

        :returns: True if the current song is not the first one.
        :rtype: bool
        """
        return self._playlist.has_previous()

    @GObject.Property(
        type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def playing(self):
        """Test if a song is currently played.

        :returns: True if a song is currently played.
        :rtype: bool
        """
        return self.props.state == Playback.PLAYING

    def _on_playlist_model_items_changed(self, model, pos, removed, added):
        if (removed > 0
                and model.get_n_items() == 0):
            self.stop()

    def _on_about_to_finish(self, klass):
        if self.props.has_next:
            self._log.debug("Song is about to finish, loading the next one.")
            next_coresong = self._playlist.get_next()
            new_url = next_coresong.props.url
            self._gst_player.props.url = new_url
            self._gapless_set = True

    def _on_eos(self, klass):
        self._playlist.next()

        if self._gapless_set:
            # After 'eos' in the gapless case, the pipeline needs to be
            # hard reset.
            self._log.debug("Song finished, loading the next one.")
            self.stop()
            self.play(self.props.current_song)
        else:
            self._log.debug("End of the playlist, stopping the player.")
            self.stop()

        self._gapless_set = False

    def _on_error(self, klass=None):
        self.stop()
        self._gapless_set = False

        current_song = self.props.current_song
        current_song.props.validation = CoreSong.Validation.FAILED
        if (self.has_next
                and self.props.repeat_mode != RepeatMode.SONG):
            self.next()

    def _on_stream_start(self, klass):
        if self._gapless_set:
            self._playlist.next()

        self._gapless_set = False
        self._time_stamp = int(time.time())

        self.emit("song-changed")

    def _load(self, coresong):
        self._log.debug("Loading song {}".format(coresong.props.title))
        self._gst_player.props.state = Playback.LOADING
        self._time_stamp = int(time.time())
        self._gst_player.props.url = coresong.props.url

    def play(self, coresong=None):
        """Play a song.

        Start playing a song, a specific CoreSong if supplied and
        available or a song in the playlist decided by the play mode.

        If a song is paused, a subsequent play call without a CoreSong
        supplied will continue playing the paused song.

        :param CoreSong coresong: The CoreSong to play or None.
        """
        if self.props.current_song is None:
            coresong = self._playlist.set_song(coresong)

        if (coresong is not None
                and coresong.props.validation == CoreSong.Validation.FAILED
                and self.props.repeat_mode != RepeatMode.SONG):
            self._on_error()
            return

        if coresong is not None:
            self._load(coresong)

        if self.props.current_song is not None:
            self._gst_player.props.state = Playback.PLAYING

    def pause(self):
        """Pause"""
        self._gst_player.props.state = Playback.PAUSED

    def stop(self):
        """Stop"""
        self._gst_player.props.state = Playback.STOPPED

    def next(self):
        """"Play next song

        Play the next song of the playlist, if any.
        """
        if self._gapless_set:
            self.set_position(0.0)
        elif self._playlist.next():
            self.play(self._playlist.props.current_song)

    def previous(self):
        """Play previous song

        Play the previous song of the playlist, if any.
        """
        position = self._gst_player.props.position
        if self._gapless_set:
            self.stop()

        if (position < 5
                and self._playlist.has_previous()):
            self._playlist.previous()
            self._gapless_set = False
            self.play(self._playlist.props.current_song)
        # This is a special case for a song that is very short and the
        # first song in the playlist. It can trigger gapless, but
        # has_previous will return False.
        elif (position < 5
                and self._playlist.props.position == 0):
            self.set_position(0.0)
            self._gapless_set = False
            self.play(self._playlist.props.current_song)
        else:
            self.set_position(0.0)

    def play_pause(self):
        """Toggle play/pause state"""
        if self.props.state == Playback.PLAYING:
            self.pause()
        else:
            self.play()

    def _on_clock_tick(self, klass, tick):
        self._log.debug("Clock tick {}, player at {} seconds".format(
            tick, self._gst_player.props.position))

        current_song = self._playlist.props.current_song

        if tick == 0:
            self._new_clock = True
            self._lastfm.now_playing(current_song)

        if self.props.duration == -1.:
            return

        position = self._gst_player.props.position
        if position > 0:
            percentage = tick / self.props.duration
            if (not self._lastfm.props.scrobbled
                    and self.props.duration > 30.
                    and (percentage > 0.5 or tick > 4 * 60)):
                self._lastfm.scrobble(current_song, self._time_stamp)

            if (percentage > 0.5
                    and self._new_clock):
                self._new_clock = False
                # FIXME: we should not need to update smart
                # playlists here but removing it may introduce
                # a bug. So, we keep it for the time being.
                # FIXME: Not using Playlist class anymore.
                # playlists.update_all_smart_playlists()
                current_song.bump_play_count()
                current_song.set_last_played()

    def _on_repeat_setting_changed(self, settings, value):
        self.props.repeat_mode = settings.get_enum('repeat')

    @GObject.Property(type=int, default=RepeatMode.NONE)
    def repeat_mode(self):
        return self._repeat

    @repeat_mode.setter  # type: ignore
    def repeat_mode(self, mode):
        if mode == self._repeat:
            return

        self._repeat = mode
        self._settings.set_enum('repeat', mode)

    @GObject.Property(type=int, default=0, flags=GObject.ParamFlags.READABLE)
    def position(self):
        """Gets current song index.

        :returns: position of the current song in the playlist.
        :rtype: int
        """
        return self._playlist.props.position

    @GObject.Property(
        type=CoreSong, default=None, flags=GObject.ParamFlags.READABLE)
    def current_song(self):
        """Get the current song.

        :returns: The song being played. None if there is no playlist.
        :rtype: CoreSong
        """
        return self._playlist.props.current_song

    def get_position(self):
        """Get player position.

        Player position in seconds.
        :returns: position
        :rtype: float
        """
        return self._gst_player.props.position

    # TODO: used by MPRIS
    def set_position(self, position_second):
        """Change GstPlayer position.

        If the position if negative, set it to zero.
        If the position if greater than song duration, do nothing
        :param float position_second: requested position in second
        """
        if position_second < 0.0:
            position_second = 0.0

        duration_second = self._gst_player.props.duration
        if position_second <= duration_second:
            self._gst_player.seek(position_second)

    def _on_seek_finished(self, klass):
        # FIXME: Just a proxy
        self.emit('seek-finished')
Beispiel #2
0
class Player(GObject.GObject):
    """Main Player object

    Contains the logic of playing a song with Music.
    """

    __gsignals__ = {
        'clock-tick': (GObject.SignalFlags.RUN_FIRST, None, (int, )),
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'prev-next-invalidated': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, (float, )),
        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, (int, )),
        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
        'volume-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    state = GObject.Property(type=int, default=Playback.STOPPED)
    duration = GObject.Property(type=float, default=-1.)

    def __repr__(self):
        return '<Player>'

    @log
    def __init__(self, parent_window):
        super().__init__()

        self._parent_window = parent_window

        self._playlist = PlayerPlaylist()
        self._playlist.connect('song-validated', self._on_song_validated)
        self._playlist.bind_property(
            'repeat-mode', self, 'repeat-mode',
            GObject.BindingFlags.SYNC_CREATE
            | GObject.BindingFlags.BIDIRECTIONAL)

        self._new_clock = True

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self._gst_player = GstPlayer()
        self._gst_player.connect('clock-tick', self._on_clock_tick)
        self._gst_player.connect('eos', self._on_eos)
        self._gst_player.bind_property('duration', self, 'duration',
                                       GObject.BindingFlags.SYNC_CREATE)
        self._gst_player.bind_property('state', self, 'state',
                                       GObject.BindingFlags.SYNC_CREATE)

        root_window = parent_window.get_toplevel()
        self._inhibit_suspend = InhibitSuspend(root_window, self)

        self._lastfm = LastFmScrobbler()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def has_next(self):
        """Test if the playlist has a next song.

        :returns: True if the current song is not the last one.
        :rtype: bool
        """
        return self._playlist.has_next()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def has_previous(self):
        """Test if the playlist has a previous song.

        :returns: True if the current song is not the first one.
        :rtype: bool
        """
        return self._playlist.has_previous()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def playing(self):
        """Test if a song is currently played.

        :returns: True if a song is currently played.
        :rtype: bool
        """
        return self.props.state == Playback.PLAYING

    @log
    def _load(self, song):
        self._gst_player.props.state = Playback.LOADING
        self._time_stamp = int(time.time())

        url_ = song.get_url()
        if url_ != self._gst_player.props.url:
            self._gst_player.props.url = url_

        self.emit('song-changed', self._playlist.get_current_index())

    @log
    def _on_eos(self, klass):
        def on_glib_idle():
            self._playlist.next()
            self.play()

        if self.props.has_next:
            GLib.idle_add(on_glib_idle)
        else:
            self.stop()

    @log
    def play(self, song_index=None):
        """Play"""
        if not self._playlist:
            return

        if (song_index is not None
                and not self._playlist.set_song(song_index)):
            return False

        if self.props.state != Playback.PAUSED:
            self._load(self._playlist.props.current_song)

        self._gst_player.props.state = Playback.PLAYING

    @log
    def pause(self):
        """Pause"""
        self._gst_player.props.state = Playback.PAUSED

    @log
    def stop(self):
        """Stop"""
        self._gst_player.props.state = Playback.STOPPED

    @log
    def next(self):
        """"Play next song

        Play the next song of the playlist, if any.
        """
        if self._playlist.next():
            self.play()

    @log
    def previous(self):
        """Play previous song

        Play the previous song of the playlist, if any.
        """
        position = self._gst_player.props.position
        if position >= 5:
            self.set_position(0.0)
            return

        if self._playlist.previous():
            self.play()

    @log
    def play_pause(self):
        """Toggle play/pause state"""
        if self.props.state == Playback.PLAYING:
            self.pause()
        else:
            self.play()

    @log
    def set_playlist(self, playlist_type, playlist_id, model, iter_):
        """Set a new playlist or change the song being played.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :param GtkListStore model: list of songs to play
        :param GtkTreeIter model_iter: requested song
        """
        playlist_changed = self._playlist.set_playlist(playlist_type,
                                                       playlist_id, model,
                                                       iter_)

        if self.props.state == Playback.PLAYING:
            self.emit('prev-next-invalidated')

        if playlist_changed:
            self.emit('playlist-changed')

    @log
    def playlist_change_position(self, prev_pos, new_pos):
        """Change order of a song in the playlist.

        :param int prev_pos: previous position
        :param int new_pos: new position
        :return: new index of the song being played. -1 if unchanged
        :rtype: int
        """
        current_index = self._playlist.change_position(prev_pos, new_pos)
        if current_index >= 0:
            self.emit('prev-next-invalidated')
        return current_index

    @log
    def remove_song(self, song_index):
        """Remove a song from the current playlist.

        :param int song_index: position of the song to remove
        """
        if self._playlist.get_current_index() == song_index:
            if self.props.has_next:
                self.next()
            elif self.props.has_previous:
                self.previous()
            else:
                self.stop()
        self._playlist.remove_song(song_index)
        self.emit('playlist-changed')
        self.emit('prev-next-invalidated')

    @log
    def add_song(self, song, song_index):
        """Add a song to the current playlist.

        :param int song_index: position of the song to add
        """
        self._playlist.add_song(song, song_index)
        self.emit('playlist-changed')
        self.emit('prev-next-invalidated')

    @log
    def _on_song_validated(self, playlist, index, status):
        self.emit('song-validated', index, status)
        return True

    @log
    def playing_playlist(self, playlist_type, playlist_id):
        """Test if the current playlist matches type and id.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :returns: True if these are the same playlists. False otherwise.
        :rtype: bool
        """
        if (playlist_type == self._playlist.props.playlist_type
                and playlist_id == self._playlist.props.playlist_id):
            return True
        return False

    @log
    def _on_clock_tick(self, klass, tick):
        logger.debug("Clock tick {}, player at {} seconds".format(
            tick, self._gst_player.props.position))

        current_song = self._playlist.props.current_song

        if tick == 0:
            self._new_clock = True
            self._lastfm.now_playing(current_song)

        if self.props.duration == -1.:
            return

        position = self._gst_player.props.position
        if position > 0:
            percentage = tick / self.props.duration
            if (not self._lastfm.scrobbled and self.props.duration > 30.
                    and (percentage > 0.5 or tick > 4 * 60)):
                self._lastfm.scrobble(current_song, self._time_stamp)

            if (percentage > 0.5 and self._new_clock):
                self._new_clock = False
                # FIXME: we should not need to update static
                # playlists here but removing it may introduce
                # a bug. So, we keep it for the time being.
                playlists.update_all_static_playlists()
                grilo.bump_play_count(current_song)
                grilo.set_last_played(current_song)

        self.emit('clock-tick', int(position))

    @GObject.Property(type=int)
    def repeat_mode(self):
        return self._repeat

    @repeat_mode.setter
    def repeat_mode(self, mode):
        self._repeat = mode
        self.emit('prev-next-invalidated')

    @GObject.Property(type=Grl.Media,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def current_song(self):
        """Get the current song.

        :returns: the song being played. None if there is no playlist.
        :rtype: Grl.Media
        """
        if not self._playlist:
            return None
        return self._playlist.props.current_song

    @log
    def get_playlist_type(self):
        """Playlist type getter

        :returns: Current playlist type. None if no playlist.
        :rtype: PlayerPlaylist.Type
        """
        return self._playlist.props.playlist_type

    @log
    def get_playlist_id(self):
        """Playlist id getter

        :returns: PlayerPlaylist identifier. None if no playlist.
        :rtype: int
        """
        return self._playlist.props.playlist_id

    @log
    def get_position(self):
        """Get player position.

        Player position in seconds.
        :returns: position
        :rtype: float
        """
        return self._gst_player.props.position

    # TODO: used by MPRIS
    @log
    def set_position(self, position_second):
        """Change GstPlayer position.

        If the position if negative, set it to zero.
        If the position if greater than song duration, do nothing
        :param float position_second: requested position in second
        """
        if position_second < 0.0:
            position_second = 0.0

        duration_second = self._gst_player.props.duration
        if position_second <= duration_second:
            self._gst_player.seek(position_second)
            self.emit('seek-finished', position_second)

    @log
    def get_volume(self):
        return self._gst_player.props.volume

    @log
    def set_volume(self, rate):
        self._gst_player.props.volume = rate
        self.emit('volume-changed')

    @log
    def get_songs(self):
        return self._playlist.get_songs()
Beispiel #3
0
class Player(GObject.GObject):
    """Main Player object

    Contains the logic of playing a song with Music.
    """

    __gsignals__ = {
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ())
    }

    state = GObject.Property(type=int, default=Playback.STOPPED)
    duration = GObject.Property(type=float, default=-1.)

    def __repr__(self):
        return '<Player>'

    @log
    def __init__(self, application):
        """Initialize the player

        :param Application application: Application object
        """
        super().__init__()

        self._app = application
        # In the case of gapless playback, both 'about-to-finish'
        # and 'eos' can occur during the same stream. 'about-to-finish'
        # already sets self._playlist to the next song, so doing it
        # again on eos would skip a song.
        # TODO: Improve playlist handling so this hack is no longer
        # needed.
        self._gapless_set = False

        self._playlist = PlayerPlaylist(self._app)

        self._settings = application.props.settings
        self._settings.connect('changed::repeat',
                               self._on_repeat_setting_changed)

        self._repeat = self._settings.get_enum('repeat')
        self.bind_property('repeat-mode', self._playlist, 'repeat-mode',
                           GObject.BindingFlags.SYNC_CREATE)

        self._new_clock = True

        self._gst_player = GstPlayer(application)
        self._gst_player.connect("about-to-finish", self._on_about_to_finish)
        self._gst_player.connect('clock-tick', self._on_clock_tick)
        self._gst_player.connect('eos', self._on_eos)
        self._gst_player.connect("error", self._on_error)
        self._gst_player.connect('seek-finished', self._on_seek_finished)
        self._gst_player.connect("stream-start", self._on_stream_start)
        self._gst_player.bind_property('duration', self, 'duration',
                                       GObject.BindingFlags.SYNC_CREATE)
        self._gst_player.bind_property('state', self, 'state',
                                       GObject.BindingFlags.SYNC_CREATE)

        self._lastfm = LastFmScrobbler()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def has_next(self):
        """Test if the playlist has a next song.

        :returns: True if the current song is not the last one.
        :rtype: bool
        """
        return self._playlist.has_next()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def has_previous(self):
        """Test if the playlist has a previous song.

        :returns: True if the current song is not the first one.
        :rtype: bool
        """
        return self._playlist.has_previous()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def playing(self):
        """Test if a song is currently played.

        :returns: True if a song is currently played.
        :rtype: bool
        """
        return self.props.state == Playback.PLAYING

    @log
    def _on_about_to_finish(self, klass):
        if self.props.has_next:
            self._playlist.next()

            new_url = self._playlist.props.current_song.props.url
            self._gst_player.props.url = new_url
            self._gapless_set = True

    @log
    def _on_eos(self, klass):
        if self._gapless_set:
            # After 'eos' in the gapless case, the pipeline needs to be
            # hard reset.
            self.stop()
            self.play()
        else:
            self.stop()

        self._gapless_set = False

    def _on_error(self, klass=None):
        self.stop()
        self._gapless_set = False

        current_song = self.props.current_song
        current_song.props.validation = CoreSong.Validation.FAILED
        if (self.has_next and self.props.repeat_mode != RepeatMode.SONG):
            self.next()

    def _on_stream_start(self, klass):
        self._gapless_set = False
        self._time_stamp = int(time.time())

        self.emit("song-changed")

    def _load(self, coresong):
        self._gst_player.props.state = Playback.LOADING
        self._time_stamp = int(time.time())
        self._gst_player.props.url = coresong.props.url

    @log
    def play(self, coresong=None):
        """Play a song.

        Load a new song or resume playback depending on song_changed
        value. If song_offset is defined, set a new song and play it.

        :param bool song_changed: indicate if a new song must be loaded
        """
        if self.props.current_song is None:
            coresong = self._playlist.set_song(coresong)

        if (coresong is not None
                and coresong.props.validation == CoreSong.Validation.FAILED
                and self.props.repeat_mode != RepeatMode.SONG):
            self._on_error()
            return

        if coresong is not None:
            self._load(coresong)

        self._gst_player.props.state = Playback.PLAYING

    @log
    def pause(self):
        """Pause"""
        self._gst_player.props.state = Playback.PAUSED

    @log
    def stop(self):
        """Stop"""
        self._gst_player.props.state = Playback.STOPPED

    @log
    def next(self):
        """"Play next song

        Play the next song of the playlist, if any.
        """
        if self._playlist.next():
            self.play(self._playlist.props.current_song)

    @log
    def previous(self):
        """Play previous song

        Play the previous song of the playlist, if any.
        """
        position = self._gst_player.props.position
        if position >= 5:
            self.set_position(0.0)
            return

        if self._playlist.previous():
            self.play(self._playlist.props.current_song)

    @log
    def play_pause(self):
        """Toggle play/pause state"""
        if self.props.state == Playback.PLAYING:
            self.pause()
        else:
            self.play()

    @log
    def playlist_change_position(self, prev_pos, new_pos):
        """Change order of a song in the playlist.

        :param int prev_pos: previous position
        :param int new_pos: new position
        :return: new index of the song being played. -1 if unchanged
        :rtype: int
        """
        current_index = self._playlist.change_position(prev_pos, new_pos)
        if current_index >= 0:
            self.emit('playlist-changed')
        return current_index

    @log
    def remove_song(self, song_index):
        """Remove a song from the current playlist.

        :param int song_index: position of the song to remove
        """
        if self.props.position == song_index:
            if self.props.has_next:
                self.next()
            elif self.props.has_previous:
                self.previous()
            else:
                self.stop()
        self._playlist.remove_song(song_index)
        self.emit('playlist-changed')

    @log
    def add_song(self, song, song_index):
        """Add a song to the current playlist.

        :param int song_index: position of the song to add
        """
        self._playlist.add_song(song, song_index)
        self.emit('playlist-changed')

    @log
    def playing_playlist(self, playlist_type, playlist_id):
        """Test if the current playlist matches type and id.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :returns: True if these are the same playlists. False otherwise.
        :rtype: bool
        """
        if (playlist_type == self._playlist.props.playlist_type
                and playlist_id == self._playlist.props.playlist_id):
            return True
        return False

    @log
    def _on_clock_tick(self, klass, tick):
        logger.debug("Clock tick {}, player at {} seconds".format(
            tick, self._gst_player.props.position))

        current_song = self._playlist.props.current_song

        if tick == 0:
            self._new_clock = True
            self._lastfm.now_playing(current_song)

        if self.props.duration == -1.:
            return

        position = self._gst_player.props.position
        if position > 0:
            percentage = tick / self.props.duration
            if (not self._lastfm.scrobbled and self.props.duration > 30.
                    and (percentage > 0.5 or tick > 4 * 60)):
                self._lastfm.scrobble(current_song, self._time_stamp)

            if (percentage > 0.5 and self._new_clock):
                self._new_clock = False
                # FIXME: we should not need to update smart
                # playlists here but removing it may introduce
                # a bug. So, we keep it for the time being.
                # FIXME: Not using Playlist class anymore.
                # playlists.update_all_smart_playlists()
                current_song.bump_play_count()
                current_song.set_last_played()

    @log
    def _on_repeat_setting_changed(self, settings, value):
        self.props.repeat_mode = settings.get_enum('repeat')

    @GObject.Property(type=int, default=RepeatMode.NONE)
    def repeat_mode(self):
        return self._repeat

    @repeat_mode.setter
    def repeat_mode(self, mode):
        if mode == self._repeat:
            return

        self._repeat = mode
        self._settings.set_enum('repeat', mode)

    @GObject.Property(type=int, default=0, flags=GObject.ParamFlags.READABLE)
    def position(self):
        """Gets current song index.

        :returns: position of the current song in the playlist.
        :rtype: int
        """
        return self._playlist.props.position

    @GObject.Property(type=CoreSong,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def current_song(self):
        """Get the current song.

        :returns: The song being played. None if there is no playlist.
        :rtype: CoreSong
        """
        return self._playlist.props.current_song

    @log
    def get_playlist_type(self):
        """Playlist type getter

        :returns: Current playlist type. None if no playlist.
        :rtype: PlayerPlaylist.Type
        """
        return self._playlist.props.playlist_type

    @log
    def get_playlist_id(self):
        """Playlist id getter

        :returns: PlayerPlaylist identifier. None if no playlist.
        :rtype: int
        """
        return self._playlist.props.playlist_id

    @log
    def get_position(self):
        """Get player position.

        Player position in seconds.
        :returns: position
        :rtype: float
        """
        return self._gst_player.props.position

    # TODO: used by MPRIS
    @log
    def set_position(self, position_second):
        """Change GstPlayer position.

        If the position if negative, set it to zero.
        If the position if greater than song duration, do nothing
        :param float position_second: requested position in second
        """
        if position_second < 0.0:
            position_second = 0.0

        duration_second = self._gst_player.props.duration
        if position_second <= duration_second:
            self._gst_player.seek(position_second)

    @log
    def _on_seek_finished(self, klass):
        # FIXME: Just a proxy
        self.emit('seek-finished')
Beispiel #4
0
class Player(GObject.GObject):
    """Main Player object

    Contains the logic of playing a song with Music.
    """

    __gsignals__ = {
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, (float, )),
        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
    }

    state = GObject.Property(type=int, default=Playback.STOPPED)
    duration = GObject.Property(type=float, default=-1.)

    def __repr__(self):
        return '<Player>'

    @log
    def __init__(self, application):
        """Initialize the player

        :param Application application: Application object
        """
        super().__init__()

        self._playlist = PlayerPlaylist()
        self._playlist.connect('song-validated', self._on_song_validated)

        self._settings = application.props.settings
        self._settings.connect('changed::repeat',
                               self._on_repeat_setting_changed)

        self._repeat = self._settings.get_enum('repeat')
        self.bind_property('repeat-mode', self._playlist, 'repeat-mode',
                           GObject.BindingFlags.SYNC_CREATE)

        self._new_clock = True

        self._gst_player = GstPlayer(application)
        self._gst_player.connect('clock-tick', self._on_clock_tick)
        self._gst_player.connect('eos', self._on_eos)
        self._gst_player.bind_property('duration', self, 'duration',
                                       GObject.BindingFlags.SYNC_CREATE)
        self._gst_player.bind_property('state', self, 'state',
                                       GObject.BindingFlags.SYNC_CREATE)

        self._lastfm = LastFmScrobbler()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def has_next(self):
        """Test if the playlist has a next song.

        :returns: True if the current song is not the last one.
        :rtype: bool
        """
        return self._playlist.has_next()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def has_previous(self):
        """Test if the playlist has a previous song.

        :returns: True if the current song is not the first one.
        :rtype: bool
        """
        return self._playlist.has_previous()

    @GObject.Property(type=bool,
                      default=False,
                      flags=GObject.ParamFlags.READABLE)
    def playing(self):
        """Test if a song is currently played.

        :returns: True if a song is currently played.
        :rtype: bool
        """
        return self.props.state == Playback.PLAYING

    @log
    def _load(self, song):
        self._gst_player.props.state = Playback.LOADING
        self._time_stamp = int(time.time())

        self._gst_player.props.url = song.get_url()

        self.emit('song-changed')

    @log
    def _on_eos(self, klass):
        if self.props.has_next:
            self.next()
        else:
            self.stop()

    @log
    def play(self, song_changed=True, song_offset=None):
        """Play a song.

        Load a new song or resume playback depending on song_changed
        value. If song_offset is defined, set a new song and play it.

        :param bool song_changed: indicate if a new song must be loaded
        :param int song_offset: position relative to current song
        """
        if self.props.current_song is None:
            return

        if (song_offset is not None
                and not self._playlist.set_song(song_offset)):
            return False

        if song_changed is True:
            self._load(self._playlist.props.current_song)

        self._gst_player.props.state = Playback.PLAYING

    @log
    def pause(self):
        """Pause"""
        self._gst_player.props.state = Playback.PAUSED

    @log
    def stop(self):
        """Stop"""
        self._gst_player.props.state = Playback.STOPPED

    @log
    def next(self):
        """"Play next song

        Play the next song of the playlist, if any.
        """
        if self._playlist.next():
            self.play()

    @log
    def previous(self):
        """Play previous song

        Play the previous song of the playlist, if any.
        """
        position = self._gst_player.props.position
        if position >= 5:
            self.set_position(0.0)
            return

        if self._playlist.previous():
            self.play()

    @log
    def play_pause(self):
        """Toggle play/pause state"""
        if self.props.state == Playback.PLAYING:
            self.pause()
        else:
            self.play(False)

    @log
    def set_playlist(self, playlist_type, playlist_id, model, iter_=None):
        """Set a new playlist or change the song being played.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :param GtkListStore model: list of songs to play
        :param GtkTreeIter model_iter: requested song
        """
        playlist_changed = self._playlist.set_playlist(playlist_type,
                                                       playlist_id, model,
                                                       iter_)

        if playlist_changed:
            self.emit('playlist-changed')

    @log
    def playlist_change_position(self, prev_pos, new_pos):
        """Change order of a song in the playlist.

        :param int prev_pos: previous position
        :param int new_pos: new position
        :return: new index of the song being played. -1 if unchanged
        :rtype: int
        """
        current_index = self._playlist.change_position(prev_pos, new_pos)
        if current_index >= 0:
            self.emit('playlist-changed')
        return current_index

    @log
    def remove_song(self, song_index):
        """Remove a song from the current playlist.

        :param int song_index: position of the song to remove
        """
        if self.props.current_song_index == song_index:
            if self.props.has_next:
                self.next()
            elif self.props.has_previous:
                self.previous()
            else:
                self.stop()
        self._playlist.remove_song(song_index)
        self.emit('playlist-changed')

    @log
    def add_song(self, song, song_index):
        """Add a song to the current playlist.

        :param int song_index: position of the song to add
        """
        self._playlist.add_song(song, song_index)
        self.emit('playlist-changed')

    @log
    def _on_song_validated(self, playlist, index, status):
        self.emit('song-validated', index, status)
        return True

    @log
    def playing_playlist(self, playlist_type, playlist_id):
        """Test if the current playlist matches type and id.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :returns: True if these are the same playlists. False otherwise.
        :rtype: bool
        """
        if (playlist_type == self._playlist.props.playlist_type
                and playlist_id == self._playlist.props.playlist_id):
            return True
        return False

    @log
    def _on_clock_tick(self, klass, tick):
        logger.debug("Clock tick {}, player at {} seconds".format(
            tick, self._gst_player.props.position))

        current_song = self._playlist.props.current_song

        if tick == 0:
            self._new_clock = True
            self._lastfm.now_playing(current_song)

        if self.props.duration == -1.:
            return

        position = self._gst_player.props.position
        if position > 0:
            percentage = tick / self.props.duration
            if (not self._lastfm.scrobbled and self.props.duration > 30.
                    and (percentage > 0.5 or tick > 4 * 60)):
                self._lastfm.scrobble(current_song, self._time_stamp)

            if (percentage > 0.5 and self._new_clock):
                self._new_clock = False
                # FIXME: we should not need to update smart
                # playlists here but removing it may introduce
                # a bug. So, we keep it for the time being.
                playlists.update_all_smart_playlists()
                grilo.bump_play_count(current_song)
                grilo.set_last_played(current_song)

    @log
    def _on_repeat_setting_changed(self, settings, value):
        self.props.repeat_mode = settings.get_enum('repeat')

    @GObject.Property(type=int, default=RepeatMode.NONE)
    def repeat_mode(self):
        return self._repeat

    @repeat_mode.setter
    def repeat_mode(self, mode):
        if mode == self._repeat:
            return

        self._repeat = mode
        self._settings.set_enum('repeat', mode)

    @GObject.Property(type=int, default=0, flags=GObject.ParamFlags.READABLE)
    def current_song_index(self):
        """Gets current song index.

        :returns: position of the current song in the playlist.
        :rtype: int
        """
        return self._playlist.props.current_song_index

    @GObject.Property(type=Grl.Media,
                      default=None,
                      flags=GObject.ParamFlags.READABLE)
    def current_song(self):
        """Get the current song.

        :returns: the song being played. None if there is no playlist.
        :rtype: Grl.Media
        """
        return self._playlist.props.current_song

    @log
    def get_playlist_type(self):
        """Playlist type getter

        :returns: Current playlist type. None if no playlist.
        :rtype: PlayerPlaylist.Type
        """
        return self._playlist.props.playlist_type

    @log
    def get_playlist_id(self):
        """Playlist id getter

        :returns: PlayerPlaylist identifier. None if no playlist.
        :rtype: int
        """
        return self._playlist.props.playlist_id

    @log
    def get_position(self):
        """Get player position.

        Player position in seconds.
        :returns: position
        :rtype: float
        """
        return self._gst_player.props.position

    # TODO: used by MPRIS
    @log
    def set_position(self, position_second):
        """Change GstPlayer position.

        If the position if negative, set it to zero.
        If the position if greater than song duration, do nothing
        :param float position_second: requested position in second
        """
        if position_second < 0.0:
            position_second = 0.0

        duration_second = self._gst_player.props.duration
        if position_second <= duration_second:
            self._gst_player.seek(position_second)
            self.emit('seek-finished', position_second)

    @log
    def get_mpris_playlist(self):
        """Get recent and next songs from the current playlist.

        If the playlist is an album, return all songs.
        Returned songs are sorted according to the repeat mode.
        This method is used by mpris to expose a TrackList.

        :returns: current playlist
        :rtype: list of index and Grl.Media
        """
        return self._playlist.get_mpris_playlist()
Beispiel #5
0
class Player(GObject.GObject):
    """Main Player object

    Contains the logic of playing a song with Music.
    """

    __gsignals__ = {
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, (float,)),
        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
    }

    state = GObject.Property(type=int, default=Playback.STOPPED)
    duration = GObject.Property(type=float, default=-1.)

    def __repr__(self):
        return '<Player>'

    @log
    def __init__(self, application):
        """Initialize the player

        :param Application application: Application object
        """
        super().__init__()

        self._playlist = PlayerPlaylist()
        self._playlist.connect('song-validated', self._on_song_validated)

        self._settings = application.props.settings
        self._settings.connect(
            'changed::repeat', self._on_repeat_setting_changed)

        self._repeat = self._settings.get_enum('repeat')
        self.bind_property(
            'repeat-mode', self._playlist, 'repeat-mode',
            GObject.BindingFlags.SYNC_CREATE)

        self._new_clock = True

        self._gst_player = GstPlayer(application)
        self._gst_player.connect('clock-tick', self._on_clock_tick)
        self._gst_player.connect('eos', self._on_eos)
        self._gst_player.bind_property(
            'duration', self, 'duration', GObject.BindingFlags.SYNC_CREATE)
        self._gst_player.bind_property(
            'state', self, 'state', GObject.BindingFlags.SYNC_CREATE)

        self._lastfm = LastFmScrobbler()

    @GObject.Property(
        type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def has_next(self):
        """Test if the playlist has a next song.

        :returns: True if the current song is not the last one.
        :rtype: bool
        """
        return self._playlist.has_next()

    @GObject.Property(
        type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def has_previous(self):
        """Test if the playlist has a previous song.

        :returns: True if the current song is not the first one.
        :rtype: bool
        """
        return self._playlist.has_previous()

    @GObject.Property(
        type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def playing(self):
        """Test if a song is currently played.

        :returns: True if a song is currently played.
        :rtype: bool
        """
        return self.props.state == Playback.PLAYING

    @log
    def _load(self, song):
        self._gst_player.props.state = Playback.LOADING
        self._time_stamp = int(time.time())

        self._gst_player.props.url = song.get_url()

        self.emit('song-changed')

    @log
    def _on_eos(self, klass):
        if self.props.has_next:
            self.next()
        else:
            self.stop()

    @log
    def play(self, song_changed=True, song_offset=None):
        """Play a song.

        Load a new song or resume playback depending on song_changed
        value. If song_offset is defined, set a new song and play it.

        :param bool song_changed: indicate if a new song must be loaded
        :param int song_offset: position relative to current song
        """
        if self.props.current_song is None:
            return

        if (song_offset is not None
                and not self._playlist.set_song(song_offset)):
            return False

        if song_changed is True:
            self._load(self._playlist.props.current_song)

        self._gst_player.props.state = Playback.PLAYING

    @log
    def pause(self):
        """Pause"""
        self._gst_player.props.state = Playback.PAUSED

    @log
    def stop(self):
        """Stop"""
        self._gst_player.props.state = Playback.STOPPED

    @log
    def next(self):
        """"Play next song

        Play the next song of the playlist, if any.
        """
        if self._playlist.next():
            self.play()

    @log
    def previous(self):
        """Play previous song

        Play the previous song of the playlist, if any.
        """
        position = self._gst_player.props.position
        if position >= 5:
            self.set_position(0.0)
            return

        if self._playlist.previous():
            self.play()

    @log
    def play_pause(self):
        """Toggle play/pause state"""
        if self.props.state == Playback.PLAYING:
            self.pause()
        else:
            self.play(False)

    @log
    def set_playlist(self, playlist_type, playlist_id, model, iter_=None):
        """Set a new playlist or change the song being played.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :param GtkListStore model: list of songs to play
        :param GtkTreeIter model_iter: requested song
        """
        playlist_changed = self._playlist.set_playlist(
            playlist_type, playlist_id, model, iter_)

        if playlist_changed:
            self.emit('playlist-changed')

    @log
    def playlist_change_position(self, prev_pos, new_pos):
        """Change order of a song in the playlist.

        :param int prev_pos: previous position
        :param int new_pos: new position
        :return: new index of the song being played. -1 if unchanged
        :rtype: int
        """
        current_index = self._playlist.change_position(prev_pos, new_pos)
        if current_index >= 0:
            self.emit('playlist-changed')
        return current_index

    @log
    def remove_song(self, song_index):
        """Remove a song from the current playlist.

        :param int song_index: position of the song to remove
        """
        if self.props.current_song_index == song_index:
            if self.props.has_next:
                self.next()
            elif self.props.has_previous:
                self.previous()
            else:
                self.stop()
        self._playlist.remove_song(song_index)
        self.emit('playlist-changed')

    @log
    def add_song(self, song, song_index):
        """Add a song to the current playlist.

        :param int song_index: position of the song to add
        """
        self._playlist.add_song(song, song_index)
        self.emit('playlist-changed')

    @log
    def _on_song_validated(self, playlist, index, status):
        self.emit('song-validated', index, status)
        return True

    @log
    def playing_playlist(self, playlist_type, playlist_id):
        """Test if the current playlist matches type and id.

        :param PlayerPlaylist.Type playlist_type: playlist type
        :param string playlist_id: unique identifer to recognize the playlist
        :returns: True if these are the same playlists. False otherwise.
        :rtype: bool
        """
        if (playlist_type == self._playlist.props.playlist_type
                and playlist_id == self._playlist.props.playlist_id):
            return True
        return False

    @log
    def _on_clock_tick(self, klass, tick):
        logger.debug("Clock tick {}, player at {} seconds".format(
            tick, self._gst_player.props.position))

        current_song = self._playlist.props.current_song

        if tick == 0:
            self._new_clock = True
            self._lastfm.now_playing(current_song)

        if self.props.duration == -1.:
            return

        position = self._gst_player.props.position
        if position > 0:
            percentage = tick / self.props.duration
            if (not self._lastfm.scrobbled
                    and self.props.duration > 30.
                    and (percentage > 0.5 or tick > 4 * 60)):
                self._lastfm.scrobble(current_song, self._time_stamp)

            if (percentage > 0.5
                    and self._new_clock):
                self._new_clock = False
                # FIXME: we should not need to update smart
                # playlists here but removing it may introduce
                # a bug. So, we keep it for the time being.
                playlists.update_all_smart_playlists()
                grilo.bump_play_count(current_song)
                grilo.set_last_played(current_song)

    @log
    def _on_repeat_setting_changed(self, settings, value):
        self.props.repeat_mode = settings.get_enum('repeat')

    @GObject.Property(type=int, default=RepeatMode.NONE)
    def repeat_mode(self):
        return self._repeat

    @repeat_mode.setter
    def repeat_mode(self, mode):
        if mode == self._repeat:
            return

        self._repeat = mode
        self._settings.set_enum('repeat', mode)

    @GObject.Property(type=int, default=0, flags=GObject.ParamFlags.READABLE)
    def current_song_index(self):
        """Gets current song index.

        :returns: position of the current song in the playlist.
        :rtype: int
        """
        return self._playlist.props.current_song_index

    @GObject.Property(
        type=Grl.Media, default=None, flags=GObject.ParamFlags.READABLE)
    def current_song(self):
        """Get the current song.

        :returns: the song being played. None if there is no playlist.
        :rtype: Grl.Media
        """
        return self._playlist.props.current_song

    @log
    def get_playlist_type(self):
        """Playlist type getter

        :returns: Current playlist type. None if no playlist.
        :rtype: PlayerPlaylist.Type
        """
        return self._playlist.props.playlist_type

    @log
    def get_playlist_id(self):
        """Playlist id getter

        :returns: PlayerPlaylist identifier. None if no playlist.
        :rtype: int
        """
        return self._playlist.props.playlist_id

    @log
    def get_position(self):
        """Get player position.

        Player position in seconds.
        :returns: position
        :rtype: float
        """
        return self._gst_player.props.position

    # TODO: used by MPRIS
    @log
    def set_position(self, position_second):
        """Change GstPlayer position.

        If the position if negative, set it to zero.
        If the position if greater than song duration, do nothing
        :param float position_second: requested position in second
        """
        if position_second < 0.0:
            position_second = 0.0

        duration_second = self._gst_player.props.duration
        if position_second <= duration_second:
            self._gst_player.seek(position_second)
            self.emit('seek-finished', position_second)

    @log
    def get_mpris_playlist(self):
        """Get recent and next songs from the current playlist.

        If the playlist is an album, return all songs.
        Returned songs are sorted according to the repeat mode.
        This method is used by mpris to expose a TrackList.

        :returns: current playlist
        :rtype: list of index and Grl.Media
        """
        return self._playlist.get_mpris_playlist()