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()
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()
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()
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
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()
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')
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()
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()
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')
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]
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()