Ejemplo n.º 1
0
    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()
Ejemplo n.º 2
0
    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()
Ejemplo n.º 3
0
    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()
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    def __init__(self, parent_window):
        super().__init__()

        self._parent_window = parent_window

        self.playlist = None
        self.playlist_type = None
        self.playlist_id = None
        self.playlist_field = None
        self.current_song = None
        self._next_song = None
        self._shuffle_history = deque(maxlen=10)
        self._new_clock = True

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self._discoverer = GstPbutils.Discoverer()
        self._discoverer.connect('discovered', self._on_discovered)
        self._discoverer.start()
        self._discovering_urls = {}

        self._settings = Gio.Settings.new('org.gnome.Music')
        self._settings.connect(
            'changed::repeat', self._on_repeat_setting_changed)
        self.repeat = self._settings.get_enum('repeat')

        self.playlist_insert_handler = 0
        self.playlist_delete_handler = 0

        self._player = GstPlayer()
        self._player.connect('clock-tick', self._on_clock_tick)
        self._player.connect('eos', self._on_eos)

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

        self._lastfm = LastFmScrobbler()
Ejemplo n.º 6
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')
Ejemplo n.º 7
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()
Ejemplo n.º 8
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()
Ejemplo n.º 9
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')
Ejemplo n.º 10
0
class Player(GObject.GObject):
    """Main Player object

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

    class Field(IntEnum):
        """Enum for player model fields"""
        SONG = 0
        DISCOVERY_STATUS = 1

    __gsignals__ = {
        'clock-tick': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-changed': (
            GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreeModel, Gtk.TreeIter)
        ),
        'playback-status-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'repeat-mode-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'volume-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'prev-next-invalidated': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'seeked': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
    }

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

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

        self._parent_window = parent_window

        self.playlist = None
        self.playlist_type = None
        self.playlist_id = None
        self.playlist_field = None
        self.current_song = None
        self._next_song = None
        self._shuffle_history = deque(maxlen=10)
        self._new_clock = True

        Gst.init(None)
        GstPbutils.pb_utils_init()

        self._discoverer = GstPbutils.Discoverer()
        self._discoverer.connect('discovered', self._on_discovered)
        self._discoverer.start()
        self._discovering_urls = {}

        self._settings = Gio.Settings.new('org.gnome.Music')
        self._settings.connect(
            'changed::repeat', self._on_repeat_setting_changed)
        self.repeat = self._settings.get_enum('repeat')

        self.playlist_insert_handler = 0
        self.playlist_delete_handler = 0

        self._player = GstPlayer()
        self._player.connect('clock-tick', self._on_clock_tick)
        self._player.connect('eos', self._on_eos)

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

        self._lastfm = LastFmScrobbler()

    @log
    def _discover_item(self, item, callback, data=None):
        url = item.get_url()
        if not url:
            logger.warning(
                "The item {} doesn't have a URL set.".format(item))
            return

        if not url.startswith("file://"):
            logger.debug(
                "Skipping discovery of {} as not a local file".format(url))
            return

        obj = (callback, data)

        if url in self._discovering_urls:
            self._discovering_urls[url] += [obj]
        else:
            self._discovering_urls[url] = [obj]
            self._discoverer.discover_uri_async(url)

    @log
    def _on_discovered(self, discoverer, info, error):
        try:
            cbs = self._discovering_urls[info.get_uri()]
            del(self._discovering_urls[info.get_uri()])

            for callback, data in cbs:
                if data is not None:
                    callback(info, error, data)
                else:
                    callback(info, error)
        except KeyError:
            # Not something we're interested in
            return

    @log
    def _on_repeat_setting_changed(self, settings, value):
        self.repeat = settings.get_enum('repeat')
        self.emit('repeat-mode-changed')
        self.emit('prev-next-invalidated')
        self._validate_next_song()

    @log
    def _on_glib_idle(self):
        self.current_song = self._next_song
        self.play()

    @log
    def add_song(self, model, path, _iter):
        """Add a song to current playlist

        :param GtkListStore model: TreeModel
        :param GtkTreePath path: song position
        :param GtkTreeIter_iter: song iter
        """
        new_row = model[_iter]
        self.playlist.insert_with_valuesv(
            int(path.to_string()),
            [self.Field.SONG, self.Field.DISCOVERY_STATUS],
            [new_row[5], new_row[11]])
        self._validate_next_song()
        self.emit('prev-next-invalidated')

    @log
    def remove_song(self, model, path):
        """Remove a song from current playlist

        :param GtkListStore model: TreeModel
        :param GtkTreePath path: song position
        """
        iter_remove = self.playlist.get_iter_from_string(path.to_string())
        if (self.current_song.get_path().to_string() == path.to_string()):
            if self.has_next():
                self.next()
            elif self.has_previous():
                self.previous()
            else:
                self.stop()

        self.playlist.remove(iter_remove)
        self._validate_next_song()
        self.emit('prev-next-invalidated')

    @log
    def _get_random_iter(self, current_song):
        first_iter = self.playlist.get_iter_first()
        if not current_song:
            current_song = first_iter
        if not current_song:
            return None
        if (hasattr(self.playlist, "iter_is_valid")
                and not self.playlist.iter_is_valid(current_song)):
            return None
        current_path = int(self.playlist.get_path(current_song).to_string())
        rows = self.playlist.iter_n_children(None)
        if rows == 1:
            return current_song
        rand = current_path
        while rand == current_path:
            rand = randint(0, rows - 1)
        return self.playlist.get_iter_from_string(str(rand))

    @log
    def _get_next_song(self):
        if (self.current_song
                and self.current_song.valid()):
            iter_ = self.playlist.get_iter(self.current_song.get_path())
        else:
            iter_ = None

        next_song = None

        if self.repeat == RepeatMode.SONG:
            if iter_:
                next_song = iter_
            else:
                next_song = self.playlist.get_iter_first()
        elif self.repeat == RepeatMode.ALL:
            if iter_:
                next_song = self.playlist.iter_next(iter_)
            if not next_song:
                next_song = self.playlist.get_iter_first()
        elif self.repeat == RepeatMode.NONE:
            if iter_:
                next_song = self.playlist.iter_next(iter_)
        elif self.repeat == RepeatMode.SHUFFLE:
            next_song = self._get_random_iter(iter_)
            if iter_:
                self._shuffle_history.append(iter_)

        if next_song:
            return Gtk.TreeRowReference.new(
                self.playlist, self.playlist.get_path(next_song))
        else:
            return None

    @log
    def _get_previous_song(self):

        @log
        def get_last_iter():
            iter_ = self.playlist.get_iter_first()
            last = None

            while iter_ is not None:
                last = iter_
                iter_ = self.playlist.iter_next(iter_)

            return last

        if (self.current_song
                and self.current_song.valid()):
            iter_ = self.playlist.get_iter(self.current_song.get_path())
        else:
            iter_ = None

        previous_song = None

        if self.repeat == RepeatMode.SONG:
            if iter_:
                previous_song = iter_
            else:
                previous_song = self.playlist.get_iter_first()
        elif self.repeat == RepeatMode.ALL:
            if iter_:
                previous_song = self.playlist.iter_previous(iter_)
            if not previous_song:
                previous_song = get_last_iter()
        elif self.repeat == RepeatMode.NONE:
            if iter_:
                previous_song = self.playlist.iter_previous(iter_)
        elif self.repeat == RepeatMode.SHUFFLE:
            if iter_:
                if (self._player.position < 5
                        and len(self._shuffle_history) > 0):
                    previous_song = self._shuffle_history.pop()

                    # Discard the current song, which is already queued
                    prev_path = self.playlist.get_path(previous_song)
                    current_path = self.playlist.get_path(iter_)
                    if prev_path == current_path:
                        previous_song = None

                if (previous_song is None
                        and len(self._shuffle_history) > 0):
                    previous_song = self._shuffle_history.pop()
                else:
                    previous_song = self._get_random_iter(iter_)

        if previous_song:
            return Gtk.TreeRowReference.new(
                self.playlist, self.playlist.get_path(previous_song))
        else:
            return None

    @log
    def has_next(self):
        repeat_modes = [RepeatMode.ALL, RepeatMode.SONG, RepeatMode.SHUFFLE]
        if (not self.playlist
                or self.playlist.iter_n_children(None) < 1):
            return False
        elif not self.current_song:
            return False
        elif self.repeat in repeat_modes:
            return True
        elif self.current_song.valid():
            tmp = self.playlist.get_iter(self.current_song.get_path())
            return self.playlist.iter_next(tmp) is not None
        else:
            return True

    @log
    def has_previous(self):
        repeat_modes = [RepeatMode.ALL, RepeatMode.SONG, RepeatMode.SHUFFLE]
        if (not self.playlist
                or self.playlist.iter_n_children(None) < 1):
            return False
        elif not self.current_song:
            return False
        elif self.repeat in repeat_modes:
            return True
        elif self.current_song.valid():
            tmp = self.playlist.get_iter(self.current_song.get_path())
            return self.playlist.iter_previous(tmp) is not None
        else:
            return True

    @GObject.Property
    def playing(self):
        """Returns if a song is currently played

        :return: playing
        :rtype: bool
        """
        return self._player.state == Playback.PLAYING

    @log
    def _load(self, media):
        self._time_stamp = int(time.time())

        url_ = media.get_url()
        if url_ != self._player.url:
            self._player.url = url_

        if self.current_song and self.current_song.valid():
            current_song = self.playlist.get_iter(
                self.current_song.get_path())
            self.emit('song-changed', self.playlist, current_song)

        self._validate_next_song()

    @log
    def _on_next_item_validated(self, _info, error, _iter):
        if error:
            logger.warning("Info {}: error: {}".format(_info, error))
            failed = DiscoveryStatus.FAILED
            self.playlist[_iter][self.Field.DISCOVERY_STATUS] = failed
            next_song = self.playlist.iter_next(_iter)

            if next_song:
                next_path = self.playlist.get_path(next_song)
                self._validate_next_song(
                    Gtk.TreeRowReference.new(self.playlist, next_path))

    @log
    def _validate_next_song(self, song=None):
        if song is None:
            song = self._get_next_song()

        self._next_song = song

        if song is None:
            return

        iter_ = self.playlist.get_iter(self._next_song.get_path())
        status = self.playlist[iter_][self.Field.DISCOVERY_STATUS]
        next_song = self.playlist[iter_][self.Field.SONG]
        url_ = next_song.get_url()

        # Skip remote songs discovery
        if (url_.startswith('http://')
                or url_.startswith('https://')):
            return False
        elif status == DiscoveryStatus.PENDING:
            self._discover_item(next_song, self._on_next_item_validated, iter_)
        elif status == DiscoveryStatus.FAILED:
            GLib.idle_add(self._validate_next_song)

        return False

    @log
    def _on_eos(self, klass):
        if self._next_song:
            GLib.idle_add(self._on_glib_idle)
        elif (self.repeat == RepeatMode.NONE):
            self.stop()

            if self.playlist is not None:
                current_song = self.playlist.get_path(
                    self.playlist.get_iter_first())
                if current_song:
                    self.current_song = Gtk.TreeRowReference.new(
                        self.playlist, current_song)
                else:
                    self.current_song = None
                self._load(self.get_current_media())
            self.emit('playback-status-changed')
        else:
            self.stop()
            self.emit('playback-status-changed')

    @log
    def play(self):
        """Play"""
        if self.playlist is None:
            return

        media = None

        if self._player.state != Playback.PAUSED:
            self.stop()

            media = self.get_current_media()
            if not media:
                return

            self._load(media)

        self._player.state = Playback.PLAYING
        self.emit('playback-status-changed')

    @log
    def pause(self):
        """Pause"""
        self._player.state = Playback.PAUSED
        self.emit('playback-status-changed')

    @log
    def stop(self):
        """Stop"""
        self._player.state = Playback.STOPPED
        self.emit('playback-status-changed')

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

        Play the next song of the playlist, if any.
        """
        if not self.has_next():
            return

        self.current_song = self._next_song
        self.play()

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

        Play the previous song of the playlist, if any.
        """
        if not self.has_previous():
            return

        position = self._player.position
        if position >= 5:
            self._player.seek(0)
            self._player.state = Playback.PLAYING
            return

        self.current_song = self._get_previous_song()
        self.play()

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

    @log
    def _create_model(self, model, model_iter):
        new_model = Gtk.ListStore(GObject.TYPE_OBJECT, GObject.TYPE_INT)
        song_id = model[model_iter][5].get_id()
        new_path = None
        for row in model:
            current_iter = new_model.insert_with_valuesv(
                -1, [self.Field.SONG, self.Field.DISCOVERY_STATUS],
                [row[5], row[11]])
            if row[5].get_id() == song_id:
                new_path = new_model.get_path(current_iter)

        return new_model, new_path

    @log
    def set_playlist(self, type_, id_, model, iter_):
        self.playlist, playlist_path = self._create_model(model, iter_)
        self.current_song = Gtk.TreeRowReference.new(
            self.playlist, playlist_path)

        if type_ != self.playlist_type or id_ != self.playlist_id:
            self.emit('playlist-changed')

        self.playlist_type = type_
        self.playlist_id = id_

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

        GLib.idle_add(self._validate_next_song)

    @log
    def playing_playlist(self, type_, id_):
        """Test if the current playlist matches type_ and id_.

        :param string type_: playlist type_
        :param string id_: unique identifer to recognize the playlist
        :returns: True if these are the same playlists. False otherwise.
        :rtype: bool
        """
        if type_ == self.playlist_type and id_ == self.playlist_id:
            return self.playlist
        else:
            return None

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

        current_media = self.get_current_media()

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

        duration = self._player.duration
        if duration is None:
            return

        position = self._player.position
        if position > 0:
            percentage = tick / duration
            if (not self._lastfm.scrobbled
                    and duration > 30
                    and (percentage > 0.5 or tick > 4 * 60)):
                self._lastfm.scrobble(current_media, 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_media)
                grilo.set_last_played(current_media)

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

    # MPRIS
    @log
    def get_gst_player(self):
        """GstPlayer getter"""
        return self._player

    @log
    def get_playback_status(self):
        # FIXME: Just a proxy right now.
        return self._player.state

    @GObject.Property
    def url(self):
        """GstPlayer url loaded

        :return: url
        :rtype: string
        """
        # FIXME: Just a proxy right now.
        return self._player.url

    @log
    def get_repeat_mode(self):
        return self.repeat

    @log
    def get_position(self):
        return self._player.position

    @log
    def set_repeat_mode(self, mode):
        self.repeat = mode
        self.emit('repeat-mode-changed')

    # TODO: used by MPRIS
    @log
    def set_position(self, offset, start_if_ne=False, next_on_overflow=False):
        if offset < 0:
            if start_if_ne:
                offset = 0
            else:
                return

        duration = self._player.duration
        if duration is None:
            return

        if duration >= offset * 1000:
            self._player.seek(offset * 1000)
            self.emit('seeked', offset)
        elif next_on_overflow:
            self.next()

    @log
    def get_volume(self):
        return self._player.volume

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

    @log
    def get_current_media(self):
        if not self.current_song or not self.current_song.valid():
            return None

        current_song = self.playlist.get_iter(self.current_song.get_path())
        failed = DiscoveryStatus.FAILED
        if self.playlist[current_song][self.Field.DISCOVERY_STATUS] == failed:
            return None
        return self.playlist[current_song][self.Field.SONG]
Ejemplo n.º 11
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()