[gnome-music/wip/jfelder/player-playlist: 3/8] player: Refactor player playlist



commit d83b7de3f7ac443d84c3532ec280369fac8d0a62
Author: Jean Felder <jfelder src gnome org>
Date:   Thu May 17 16:45:54 2018 +0200

    player: Refactor player playlist
    
    Separate player logic from the playlist logic.
    Rename discovery logic to validation as this is more accurate of the
    underlying logic.
    PlayerPlaylist object handles the playlist and songs validation
    logic. Player object acts as a glue for the ui and the underlying
    logic (playlist and validation).
    Use Gobject Properties.
    
    There are 4 ways to launch a song:
    1. Click on a song in the current view. The set_playlist method is
    called.
    2. At the end of the current song. The next method is automatically called.
    3. click on the next buttom from the PlayerToolbar. The next method is called.
    4. click on the previous buttom from the PlayerToolbar. The previous
    method is called.
    
    Validation is a very expensive operation, so only do it when it's
    needed. See commit message from 6f1cb8d4.
    
    The Validation logic brings 3 features:
    - display an error icon if a song cannot be played
    - do not load a song that cannot be played
    - when the song changes, try to load the next possible one: if the
    next song cannot be loaded, try the one after, etc.
    
    In "set_playlist" method, if the song has already been played the
    validation information is already known, nothing to add. If the song
    has never been played, there is no information yet. In that case,
    validate_current_song and validate_next_song need to be called to
    trigger the validation mechanism.
    
    In "next" method, call validate_next_song to continue the validation
    mechanism if there is a next song.
    
    In "previous" method, call validate_previous_song to continue the validation
    mechanism if there is a previous song.
    
    Closes: #60, #154

 gnomemusic/inhibitsuspend.py             |   4 +-
 gnomemusic/mpris.py                      |  99 ++--
 gnomemusic/player.py                     | 926 +++++++++++++++++++------------
 gnomemusic/views/artistsview.py          |   4 +-
 gnomemusic/views/playlistview.py         |  90 +--
 gnomemusic/views/searchview.py           |   9 +-
 gnomemusic/views/songsview.py            |  33 +-
 gnomemusic/widgets/albumwidget.py        |  28 +-
 gnomemusic/widgets/artistalbumswidget.py |  21 +-
 gnomemusic/widgets/artistalbumwidget.py  |   5 +-
 gnomemusic/widgets/playertoolbar.py      |  29 +-
 gnomemusic/window.py                     |  12 +-
 12 files changed, 734 insertions(+), 526 deletions(-)
---
diff --git a/gnomemusic/inhibitsuspend.py b/gnomemusic/inhibitsuspend.py
index 0359daed..daf69aba 100644
--- a/gnomemusic/inhibitsuspend.py
+++ b/gnomemusic/inhibitsuspend.py
@@ -86,12 +86,12 @@ class InhibitSuspend(GObject.GObject):
         if self._player.get_playback_status() == Playback.PLAYING:
             self._inhibit_suspend()
 
-        # TODO: The additional check for has_next() is necessary
+        # TODO: The additional check for has_next property is necessary
         # since after a track is done, the player
         # goes into STOPPED state before it goes back to PLAYING.
         # To be simplified when the player's behavior is corrected.
 
         if (self._player.get_playback_status() == Playback.PAUSED
                 or (self._player.get_playback_status() == Playback.STOPPED
-                    and not self._player.has_next())):
+                    and not self._player.props.has_next)):
             self._uninhibit_suspend()
diff --git a/gnomemusic/mpris.py b/gnomemusic/mpris.py
index fdc4a647..252ca90a 100644
--- a/gnomemusic/mpris.py
+++ b/gnomemusic/mpris.py
@@ -26,7 +26,7 @@
 import codecs
 
 from gnomemusic.gstplayer import Playback
-from gnomemusic.player import RepeatMode
+from gnomemusic.player import PlayerField, PlayerPlaylist, RepeatMode
 from gnomemusic.grilo import grilo
 from gnomemusic.playlists import Playlists
 from gnomemusic.utils import View
@@ -238,9 +238,6 @@ class MediaPlayer2Service(Server):
         playlists.connect('playlist-deleted', self._on_playlists_count_changed)
         grilo.connect('ready', self._on_grilo_ready)
         self.playlists = []
-        self.playlist = None
-        self.playlist_insert_handler = 0
-        self.playlist_delete_handler = 0
         self.first_song_handler = 0
 
     @log
@@ -255,9 +252,9 @@ class MediaPlayer2Service(Server):
 
     @log
     def _get_loop_status(self):
-        if self.player.repeat == RepeatMode.NONE:
+        if self.player.props.repeat_mode == RepeatMode.NONE:
             return 'None'
-        elif self.player.repeat == RepeatMode.SONG:
+        elif self.player.props.repeat_mode == RepeatMode.SONG:
             return 'Track'
         else:
             return 'Playlist'
@@ -265,7 +262,7 @@ class MediaPlayer2Service(Server):
     @log
     def _get_metadata(self, media=None):
         if not media:
-            media = self.player.get_current_media()
+            media = self.player.props.current_song
         if not media:
             return {}
 
@@ -353,17 +350,17 @@ class MediaPlayer2Service(Server):
 
     @log
     def _get_media_from_id(self, track_id):
-        for track in self.player.playlist:
-            media = track[self.player.Field.SONG]
+        for track in self.player.get_songs():
+            media = track[PlayerField.SONG]
             if track_id == self._get_media_id(media):
                 return media
         return None
 
     @log
     def _get_track_list(self):
-        if self.player.playlist:
-            return [self._get_media_id(track[self.player.Field.SONG])
-                    for track in self.player.playlist]
+        if self.player.props.playing:
+            return [self._get_media_id(song[PlayerField.SONG])
+                    for song in self.player.get_songs()]
         else:
             return []
 
@@ -400,16 +397,19 @@ class MediaPlayer2Service(Server):
 
     @log
     def _get_active_playlist(self):
-        playlist = self._get_playlist_from_id(self.player.playlist_id) \
-            if self.player.playlist_type == 'Playlist' else None
-        playlistName = utils.get_media_title(playlist) \
-            if playlist else ''
-        return (playlist is not None,
-                (self._get_playlist_path(playlist), playlistName, ''))
+        playlist = None
+        playlist_name = ''
+        if self.player.get_playlist_type() == PlayerPlaylist.Type.PLAYLIST:
+            playlist = self._get_playlist_from_id(
+                self.player.get_playlist_id())
+            playlist_name = utils.get_media_title(playlist)
+
+        path = self._get_playlist_path(playlist)
+        return (playlist is not None, (path, playlist_name, ''))
 
     @log
-    def _on_current_song_changed(self, player, current_iter, data=None):
-        if self.player.repeat == RepeatMode.SONG:
+    def _on_current_song_changed(self, player, position):
+        if self.player.props.repeat_mode == RepeatMode.SONG:
             self.Seeked(0)
 
         self.PropertiesChanged(MediaPlayer2Service.MEDIA_PLAYER2_PLAYER_IFACE,
@@ -441,7 +441,7 @@ class MediaPlayer2Service(Server):
         self.PropertiesChanged(MediaPlayer2Service.MEDIA_PLAYER2_PLAYER_IFACE,
                                {
                                    'LoopStatus': GLib.Variant('s', self._get_loop_status()),
-                                   'Shuffle': GLib.Variant('b', self.player.repeat == RepeatMode.SHUFFLE),
+                                   'Shuffle': GLib.Variant('b', self.player.props.repeat_mode == 
RepeatMode.SHUFFLE),
                                },
                                [])
 
@@ -457,8 +457,8 @@ class MediaPlayer2Service(Server):
     def _on_prev_next_invalidated(self, player, data=None):
         self.PropertiesChanged(MediaPlayer2Service.MEDIA_PLAYER2_PLAYER_IFACE,
                                {
-                                   'CanGoNext': GLib.Variant('b', self.player.has_next()),
-                                   'CanGoPrevious': GLib.Variant('b', self.player.has_previous()),
+                                   'CanGoNext': GLib.Variant('b', self.player.props.has_next),
+                                   'CanGoPrevious': GLib.Variant('b', self.player.props.has_previous),
                                },
                                [])
 
@@ -467,7 +467,7 @@ class MediaPlayer2Service(Server):
         if self.first_song_handler:
             model.disconnect(self.first_song_handler)
             self.first_song_handler = 0
-        self.player.set_playlist('Songs', None, model, iter_)
+        self.player.set_playlist(PlayerPlaylist.Type.SONG, None, model, iter_)
         self.player.play()
 
     @log
@@ -476,13 +476,6 @@ class MediaPlayer2Service(Server):
 
     @log
     def _on_playlist_changed(self, player, data=None):
-        if self.playlist:
-            if self.playlist_insert_handler:
-                self.playlist.disconnect(self.playlist_insert_handler)
-            if self.playlist_delete_handler:
-                self.playlist.disconnect(self.playlist_delete_handler)
-
-        self.playlist = self.player.playlist
         self._on_playlist_modified()
 
         self.PropertiesChanged(MediaPlayer2Service.MEDIA_PLAYER2_PLAYLISTS_IFACE,
@@ -491,18 +484,12 @@ class MediaPlayer2Service(Server):
                                },
                                [])
 
-        self.playlist_insert_handler = \
-            self.playlist.connect('row-inserted', self._on_playlist_modified)
-        self.playlist_delete_handler = \
-            self.playlist.connect('row-deleted', self._on_playlist_modified)
-
     @log
     def _on_playlist_modified(self, path=None, _iter=None, data=None):
-        if self.player.current_song and self.player.current_song.valid():
-            path = self.player.current_song.get_path()
-            current_song = self.player.playlist[path][self.player.Field.SONG]
+        if self.player.props.current_song:
             track_list = self._get_track_list()
-            self.TrackListReplaced(track_list, self._get_media_id(current_song))
+            self.TrackListReplaced(
+                track_list, self._get_media_id(self.player.props.current_song))
             self.PropertiesChanged(MediaPlayer2Service.MEDIA_PLAYER2_TRACKLIST_IFACE,
                                    {
                                        'Tracks': GLib.Variant('ao', track_list),
@@ -551,7 +538,7 @@ class MediaPlayer2Service(Server):
         self.player.stop()
 
     def Play(self):
-        if self.player.playlist is not None:
+        if self.player.get_songs():
             self.player.play()
         elif self.first_song_handler == 0:
             window = self.app.get_active_window()
@@ -594,13 +581,9 @@ class MediaPlayer2Service(Server):
         pass
 
     def GoTo(self, track_id):
-        for track in self.player.playlist:
-            media = track[self.player.Field.SONG]
-            if track_id == self._get_media_id(media):
-                self.player.set_playlist(
-                    self.player.playlist_type, self.player.playlist_id,
-                    self.player.playlist, track.iter)
-                self.player.play()
+        for index, song in enumerate(self.player.get_songs()):
+            if track_id == self._get_media_id(song[PlayerField.SONG]):
+                self.player_play(index)
                 return
 
     def TrackListReplaced(self, tracks, current_song):
@@ -682,16 +665,16 @@ class MediaPlayer2Service(Server):
                 'PlaybackStatus': GLib.Variant('s', self._get_playback_status()),
                 'LoopStatus': GLib.Variant('s', self._get_loop_status()),
                 'Rate': GLib.Variant('d', 1.0),
-                'Shuffle': GLib.Variant('b', self.player.repeat == RepeatMode.SHUFFLE),
+                'Shuffle': GLib.Variant('b', self.player.props.repeat_mode == RepeatMode.SHUFFLE),
                 'Metadata': GLib.Variant('a{sv}', self._get_metadata()),
                 'Volume': GLib.Variant('d', self.player.get_volume()),
                 'Position': GLib.Variant('x', self.player.get_position()),
                 'MinimumRate': GLib.Variant('d', 1.0),
                 'MaximumRate': GLib.Variant('d', 1.0),
-                'CanGoNext': GLib.Variant('b', self.player.has_next()),
-                'CanGoPrevious': GLib.Variant('b', self.player.has_previous()),
-                'CanPlay': GLib.Variant('b', self.player.current_song is not None),
-                'CanPause': GLib.Variant('b', self.player.current_song is not None),
+                'CanGoNext': GLib.Variant('b', self.player.props.has_next),
+                'CanGoPrevious': GLib.Variant('b', self.player.props.has_previous),
+                'CanPlay': GLib.Variant('b', self.player.props.current_song is not None),
+                'CanPause': GLib.Variant('b', self.player.props.current_song is not None),
                 'CanSeek': GLib.Variant('b', True),
                 'CanControl': GLib.Variant('b', True),
             }
@@ -727,16 +710,16 @@ class MediaPlayer2Service(Server):
                 self.player.set_volume(new_value)
             elif property_name == 'LoopStatus':
                 if new_value == 'None':
-                    self.player.set_repeat_mode(RepeatMode.NONE)
+                    self.player.props.repeat_mode = RepeatMode.NONE
                 elif new_value == 'Track':
-                    self.player.set_repeat_mode(RepeatMode.SONG)
+                    self.player.props.repeat_mode = RepeatMode.SONG
                 elif new_value == 'Playlist':
-                    self.player.set_repeat_mode(RepeatMode.ALL)
+                    self.player.props.repeat_mode = RepeatMode.ALL
             elif property_name == 'Shuffle':
                 if new_value:
-                    self.player.set_repeat_mode(RepeatMode.SHUFFLE)
+                    self.player.props.repeat_mode = RepeatMode.SHUFFLE
                 else:
-                    self.player.set_repeat_mode(RepeatMode.NONE)
+                    self.player.props.repeat_mode = RepeatMode.NONE
         else:
             raise Exception(
                 'org.mpris.MediaPlayer2.GnomeMusic',
diff --git a/gnomemusic/player.py b/gnomemusic/player.py
index 7de6b765..56e353d2 100644
--- a/gnomemusic/player.py
+++ b/gnomemusic/player.py
@@ -22,9 +22,9 @@
 # code, but you are not obligated to do so.  If you do not wish to do so,
 # delete this exception statement from your version.
 
-from collections import deque
+from collections import defaultdict
 from enum import IntEnum
-from random import randint
+from random import shuffle, randrange
 import logging
 import time
 
@@ -32,7 +32,7 @@ import gi
 gi.require_version('Gst', '1.0')
 gi.require_version('GstAudio', '1.0')
 gi.require_version('GstPbutils', '1.0')
-from gi.repository import Gtk, GLib, Gio, GObject, Gst, GstPbutils
+from gi.repository import Gio, GLib, GObject, Grl, Gst, GstPbutils
 
 from gnomemusic import log
 from gnomemusic.gstplayer import GstPlayer, Playback
@@ -54,407 +54,546 @@ class RepeatMode(IntEnum):
     SHUFFLE = 3
 
 
-class DiscoveryStatus:
+class ValidationStatus(IntEnum):
+    """Enum for song validation"""
     PENDING = 0
     FAILED = 1
     SUCCEEDED = 2
 
 
-class Player(GObject.GObject):
-    """Main Player object
+class PlayerField(IntEnum):
+    """Enum for player model fields"""
+    SONG = 0
+    VALIDATION = 1
 
-    Contains the logic of playing a song with Music.
+
+class PlayerPlaylist(GObject.GObject):
+    """PlayerPlaylist object
+
+    Contains the logic to validate a song, handle RepeatMode and the
+    list of songs being played.
     """
 
-    class Field(IntEnum):
-        """Enum for player model fields"""
-        SONG = 0
-        DISCOVERY_STATUS = 1
+    class Type(IntEnum):
+        """Type of playlist."""
+        SONGS = 0
+        ALBUM = 1
+        ARTIST = 2
+        PLAYLIST = 3
+        SEARCH_RESULT = 4
 
     __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,)),
+        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
     }
 
     def __repr__(self):
-        return '<Player>'
+        return '<PlayerPlayList>'
 
     @log
-    def __init__(self, parent_window):
+    def __init__(self):
         super().__init__()
+        self._songs = []
+        self._shuffle_indexes = []
+        self._current_index = 0
 
-        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
+        self._type = -1
+        self._id = -1
 
-        Gst.init(None)
-        GstPbutils.pb_utils_init()
+        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._validation_indexes = None
         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')
+    @log
+    def set_playlist(self, playlist_type, playlist_id, model, model_iter):
+        """Set a new playlist or change the song being played
 
-        self.playlist_insert_handler = 0
-        self.playlist_delete_handler = 0
+        :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
 
-        self._player = GstPlayer()
-        self._player.connect('clock-tick', self._on_clock_tick)
-        self._player.connect('eos', self._on_eos)
+        :return: True if the playlist has been updated. False otherwise
+        :rtype: bool
+        """
+        path = model.get_path(model_iter)
+        self._current_index = int(path.to_string())
+        self._validation_indexes = defaultdict(list)
+
+        # Playlist is the same. Check that the requested song is valid.
+        # If not, try to get the next valid one
+        if (playlist_type == self._type
+                and playlist_id == self._id):
+            if not self._current_song_is_valid():
+                self.next()
+            else:
+                self._validate_song(self._current_index)
+                self._validate_next_song()
+            return False
 
-        root_window = parent_window.get_toplevel()
-        self._inhibit_suspend = InhibitSuspend(root_window, self)
+        self._type = playlist_type
+        self._id = playlist_id
 
-        self._lastfm = LastFmScrobbler()
+        self._songs = []
+        for row in model:
+            self._songs.append([row[5], row[11]])
+
+        if self._repeat == RepeatMode.SHUFFLE:
+            self._shuffle_indexes = list(range(len(self._songs)))
+            shuffle(self._shuffle_indexes)
+            self._shuffle_indexes.remove(self._current_index)
+            self._shuffle_indexes.insert(0, self._current_index)
+
+        # If the playlist has already been played, check that the requested
+        # song is valid. If it has never been played, validate the current
+        # song and the next song to display an error icon on failure.
+        if not self._current_song_is_valid():
+            self.next()
+        else:
+            self._validate_song(self._current_index)
+            self._validate_next_song()
+        return True
 
     @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))
+    def set_song(self, song_index):
+        """Change playlist index.
+
+        :param int song_index: requested song index
+        :return: True if the index has changed. False otherwise.
+        :rtype: bool
+        """
+        if song_index >= len(self._songs):
+            return False
+
+        self._current_index = song_index
+        return True
+
+    @log
+    def 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_item = self._songs[self._current_index]
+        current_song_id = current_item[PlayerField.SONG].get_id()
+        changed_song = self._songs.pop(prev_pos)
+        self._songs.insert(new_pos, changed_song)
+
+        # Update current_index if necessary.
+        return_index = -1
+        first_pos = min(prev_pos, new_pos)
+        last_pos = max(prev_pos, new_pos)
+        if (self._current_index >= first_pos
+                and self._current_index <= last_pos):
+            for index, item in enumerate(self._songs[first_pos:last_pos + 1]):
+                if item[PlayerField.SONG].get_id() == current_song_id:
+                    self._current_index = first_pos + index
+                    return_index = self._current_index
+                    break
+
+        if self._repeat == RepeatMode.SHUFFLE:
+            index_l = self._shuffle_indexes.index(last_pos)
+            self._shuffle_indexes.pop(index_l)
+            self._shuffle_indexes = [
+                index + 1 if (index < last_pos and index >= first_pos)
+                else index
+                for index in self._shuffle_indexes]
+            self._shuffle_indexes.insert(index_l, first_pos)
+
+        return return_index
+
+    @log
+    def add_song(self, song, song_index):
+        """Add a song to the playlist.
+
+        :param Grl.Media song: new song
+        :param int song_index: song position
+        """
+        item = [song, ValidationStatus.PENDING]
+        self._songs.insert(song_index, item)
+        if song_index >= self._current_index:
+            self._current_index += 1
+
+        self._validate_song(song_index)
+
+        # In the shuffle case, insert song at a random position which
+        # has not been played yet.
+        if self._repeat == RepeatMode.SHUFFLE:
+            index = self._shuffle_indexes.index(self._current_index)
+            new_song_index = randrange(index, len(self._shuffle_indexes))
+            self._shuffle_indexes.insert(new_song_index, song_index)
+
+    @log
+    def remove_song(self, song_index):
+        """Remove a song from the playlist.
+
+        :param int song_index: index of the song to remove
+        """
+        self._songs.pop(song_index)
+        if song_index < self._current_index:
+            self._current_index -= 1
+
+        if self._repeat == RepeatMode.SHUFFLE:
+            self._shuffle_indexes.remove(song_index)
+            self._shuffle_indexes = [
+                index - 1 if index > song_index else index
+                for index in self._shuffle_indexes]
+
+    @log
+    def _on_repeat_setting_changed(self, settings, value):
+        self.props.repeat_mode = settings.get_enum('repeat')
+
+    @log
+    def _on_discovered(self, discoverer, info, error):
+        url = info.get_uri()
+        field = PlayerField.VALIDATION
+        index = self._validation_indexes[url].pop(0)
+        if not self._validation_indexes[url]:
+            self._validation_indexes.pop(url)
+
+        if error:
+            logger.warning("Info {}: error: {}".format(info, error))
+            self._songs[index][field] = ValidationStatus.FAILED
+        else:
+            self._songs[index][field] = ValidationStatus.SUCCEEDED
+        self.emit('song-validated', index, self._songs[index][field])
+
+    @log
+    def _validate_song(self, index):
+        item = self._songs[index]
+        # Song has already been processed, nothing to do.
+        if item[PlayerField.VALIDATION] != ValidationStatus.PENDING:
             return
 
+        song = item[PlayerField.SONG]
+        url = song.get_url()
+        if not url:
+            logger.warning("The item {} doesn't have a URL set.".format(song))
+            return
         if not url.startswith("file://"):
             logger.debug(
-                "Skipping discovery of {} as not a local file".format(url))
+                "Skipping validation of {} as not a local file".format(url))
             return
 
-        obj = (callback, data)
+        self._validation_indexes[url].append(index)
+        self._discoverer.discover_uri_async(url)
 
-        if url in self._discovering_urls:
-            self._discovering_urls[url] += [obj]
+    @log
+    def _get_next_index(self):
+        if not self.has_next():
+            return -1
+
+        if self._repeat == RepeatMode.SONG:
+            return self._current_index
+        if (self._repeat == RepeatMode.ALL
+                and self._current_index == (len(self._songs) - 1)):
+            return 0
+        if self._repeat == RepeatMode.SHUFFLE:
+            index = self._shuffle_indexes.index(self._current_index)
+            return self._shuffle_indexes[index + 1]
         else:
-            self._discovering_urls[url] = [obj]
-            self._discoverer.discover_uri_async(url)
+            return self._current_index + 1
 
     @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
+    def _get_previous_index(self):
+        if not self.has_previous():
+            return -1
+
+        if self._repeat == RepeatMode.SONG:
+            return self._current_index
+        if (self._repeat == RepeatMode.ALL
+                and self._current_index == 0):
+            return len(self._songs) - 1
+        if self._repeat == RepeatMode.SHUFFLE:
+            index = self._shuffle_indexes.index(self._current_index)
+            return self._shuffle_indexes[index - 1]
+        else:
+            return self._current_index - 1
+
+    @log
+    def _validate_next_song(self):
+        if self._repeat == RepeatMode.SONG:
             return
 
+        next_index = self._get_next_index()
+        if next_index >= 0:
+            self._validate_song(next_index)
+
     @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()
+    def _validate_previous_song(self):
+        if self._repeat == RepeatMode.SONG:
+            return
+
+        previous_index = self._get_previous_index()
+        if previous_index >= 0:
+            self._validate_song(previous_index)
 
     @log
-    def _on_glib_idle(self):
-        self.current_song = self._next_song
-        self.play()
+    def has_next(self):
+        """Test if there is a song after the current one.
+
+        :return: True if there is a song. False otherwise.
+        :rtype: bool
+        """
+        if (self._repeat == RepeatMode.SHUFFLE
+                and self._shuffle_indexes):
+            index = self._shuffle_indexes.index(self._current_index)
+            return index < (len(self._shuffle_indexes) - 1)
+        if self._repeat != RepeatMode.NONE:
+            return True
+        return self._current_index < (len(self._songs) - 1)
 
     @log
-    def add_song(self, model, path, _iter):
-        """Add a song to current playlist
+    def has_previous(self):
+        """Test if there is a song before the current one.
 
-        :param GtkListStore model: TreeModel
-        :param GtkTreePath path: song position
-        :param GtkTreeIter_iter: song iter
+        :return: True if there is a song. False otherwise.
+        :rtype: bool
         """
-        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')
+        if (self._repeat == RepeatMode.SHUFFLE
+                and self._shuffle_indexes):
+            index = self._shuffle_indexes.index(self._current_index)
+            return index > 0
+        if self._repeat != RepeatMode.NONE:
+            return True
+        return self._current_index > 0
 
     @log
-    def remove_song(self, model, path):
-        """Remove a song from current playlist
+    def next(self):
+        """Go to the next song in the playlist.
 
-        :param GtkListStore model: TreeModel
-        :param GtkTreePath path: song position
+        :return: True if the operation succeeded. False otherwise.
+        :rtype: bool
         """
-        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()
+        next_index = self._get_next_index()
+        if next_index >= 0:
+            self._current_index = next_index
+            if self._current_song_is_valid():
+                self._validate_next_song()
+                return True
             else:
-                self.stop()
-
-        self.playlist.remove(iter_remove)
-        self._validate_next_song()
-        self.emit('prev-next-invalidated')
+                return self.next()
+        return False
 
     @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
+    def previous(self):
+        """Go to the previous song in the playlist.
 
-        if self.repeat == RepeatMode.SONG:
-            if iter_:
-                next_song = iter_
+        :return: True if the operation succeeded. False otherwise.
+        :rtype: bool
+        """
+        previous_index = self._get_previous_index()
+        if previous_index >= 0:
+            self._current_index = previous_index
+            if self._current_song_is_valid():
+                self._validate_previous_song()
+                return True
             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
+                return self.previous()
+        return False
 
     @log
-    def _get_previous_song(self):
+    def get_current_index(self):
+        """Get current song index.
 
-        @log
-        def get_last_iter():
-            iter_ = self.playlist.get_iter_first()
-            last = None
+        :returns: position of the current song int the playlist.
+        :rtype: int
+        """
+        return self._current_index
 
-            while iter_ is not None:
-                last = iter_
-                iter_ = self.playlist.iter_next(iter_)
+    @GObject.Property(
+        type=Grl.Media, default=None, flags=GObject.ParamFlags.READABLE)
+    def current_song(self):
+        """Get current song.
 
-            return last
+        :returns: the song being played or None if there are no songs
+        :rtype: Grl.Media
+        """
+        if self._songs:
+            return self._songs[self._current_index][PlayerField.SONG]
+        return None
 
-        if (self.current_song
-                and self.current_song.valid()):
-            iter_ = self.playlist.get_iter(self.current_song.get_path())
-        else:
-            iter_ = None
+    def _current_song_is_valid(self):
+        """Check if current song can be played.
 
-        previous_song = None
+        :returns: False if validation failed
+        :rtype: bool
+        """
+        current_item = self._songs[self._current_index]
+        return current_item[PlayerField.VALIDATION] != ValidationStatus.FAILED
 
-        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
+    @GObject.Property(type=int, default=RepeatMode.NONE)
+    def repeat_mode(self):
+        """Get repeat mode.
 
-    @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
+        :returns: the repeat mode
+        :rtype: RepeatMode
+        """
+        return self._repeat
 
-    @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
+    @repeat_mode.setter
+    def repeat_mode(self, mode):
+        """Set repeat mode.
 
-    @GObject.Property
-    def playing(self):
-        """Returns if a song is currently played
+        :param RepeatMode mode: new repeat_mode
+        """
+        if (mode == RepeatMode.SHUFFLE
+                and self._songs):
+            self._shuffle_indexes = list(range(len(self._songs)))
+            shuffle(self._shuffle_indexes)
+            self._shuffle_indexes.remove(self._current_index)
+            self._shuffle_indexes.insert(0, self._current_index)
 
-        :return: playing
-        :rtype: bool
+        self._repeat = mode
+
+    @GObject.Property(type=int, flags=GObject.ParamFlags.READABLE)
+    def playlist_id(self):
+        """Get playlist unique identifier.
+
+        :returns: playlist id
+        :rtype: int
         """
-        return self._player.state == Playback.PLAYING
+        return self._id
+
+    @GObject.Property(type=int, flags=GObject.ParamFlags.READABLE)
+    def playlist_type(self):
+        """Get playlist type.
+
+        :returns: playlist type
+        :rtype: PlayerPlaylist.Type
+        """
+        return self._type
 
     @log
-    def _load(self, media):
-        self._time_stamp = int(time.time())
+    def get_songs(self):
+        """Get the current playlist.
 
-        url_ = media.get_url()
-        if url_ != self._player.url:
-            self._player.url = url_
+        Each member of the list has two elements: the song, and the validation
+        status.
 
-        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)
+        :returns: current playlist
+        :rtype: list
+        """
+        return self._songs
 
-        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)
+class Player(GObject.GObject):
+    """Main Player object
+
+    Contains the logic of playing a song with Music.
+    """
 
-            if next_song:
-                next_path = self.playlist.get_path(next_song)
-                self._validate_next_song(
-                    Gtk.TreeRowReference.new(self.playlist, next_path))
+    __gsignals__ = {
+        'clock-tick': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
+        'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
+        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
+        '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 _validate_next_song(self, song=None):
-        if song is None:
-            song = self._get_next_song()
+    def __init__(self, parent_window):
+        super().__init__()
 
-        self._next_song = song
+        self._parent_window = parent_window
 
-        if song is None:
-            return
+        self._playlist = PlayerPlaylist()
+        self._playlist.connect('song-validated', self._on_song_validated)
 
-        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()
+        self._new_clock = True
 
-        # 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)
+        Gst.init(None)
+        GstPbutils.pb_utils_init()
 
-        return False
+        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()
+
+    @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._player.state == Playback.PLAYING
+
+    @log
+    def _load(self, song):
+        self._time_stamp = int(time.time())
+
+        url_ = song.get_url()
+        if url_ != self._player.url:
+            self._player.url = url_
+
+        self.emit('song-changed', self._playlist.get_current_index())
 
     @log
     def _on_eos(self, klass):
-        if self._next_song:
-            GLib.idle_add(self._on_glib_idle)
-        elif (self.repeat == RepeatMode.NONE):
-            self.stop()
+        def on_glib_idle():
+            self._playlist.next()
+            self.play()
 
-            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')
+        if self.props.has_next:
+            GLib.idle_add(on_glib_idle)
         else:
             self.stop()
             self.emit('playback-status-changed')
 
     @log
-    def play(self):
+    def play(self, song_index=None):
         """Play"""
-        if self.playlist is None:
+        if not self._playlist:
             return
 
-        media = None
+        if (song_index
+                and not self._playlist.set_song(song_index)):
+            return False
 
         if self._player.state != Playback.PAUSED:
             self.stop()
-
-            media = self.get_current_media()
-            if not media:
-                return
-
-            self._load(media)
+            self._load(self._playlist.props.current_song)
 
         self._player.state = Playback.PLAYING
         self.emit('playback-status-changed')
@@ -477,11 +616,9 @@ class Player(GObject.GObject):
 
         Play the next song of the playlist, if any.
         """
-        if not self.has_next():
-            return
 
-        self.current_song = self._next_song
-        self.play()
+        if self._playlist.next():
+            self.play()
 
     @log
     def previous(self):
@@ -489,17 +626,14 @@ class Player(GObject.GObject):
 
         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()
+        if self._playlist.previous():
+            self.play()
 
     @log
     def play_pause(self):
@@ -510,60 +644,97 @@ class Player(GObject.GObject):
             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)
+    def set_playlist(self, playlist_type, playlist_id, model, iter_):
+        """Set a new playlist or change the song being played.
 
-        return new_model, new_path
+        :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_)
 
-    @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 self._player.state == Playback.PLAYING:
+            self.emit('prev-next-invalidated')
+
+        self._playlist.bind_property(
+            'repeat_mode', self, 'repeat_mode',
+            GObject.BindingFlags.SYNC_CREATE)
 
-        if type_ != self.playlist_type or id_ != self.playlist_id:
+        if playlist_changed:
             self.emit('playlist-changed')
 
-        self.playlist_type = type_
-        self.playlist_id = id_
+    @log
+    def playlist_change_position(self, prev_pos, new_pos):
+        """Change order of a song in the playlist.
 
-        if self._player.state == Playback.PLAYING:
+        :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')
 
-        GLib.idle_add(self._validate_next_song)
+    @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 playing_playlist(self, type_, id_):
-        """Test if the current playlist matches type_ and id_.
+    def _on_song_validated(self, playlist, index, status):
+        self.emit('song-validated', index, status)
+        return True
 
-        :param string type_: playlist type_
-        :param string id_: unique identifer to recognize the playlist
+    @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 type_ == self.playlist_type and id_ == self.playlist_id:
-            return self.playlist
-        else:
-            return None
+        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._player.position))
 
-        current_media = self.get_current_media()
+        current_song = self._playlist.props.current_song
 
         if tick == 0:
             self._new_clock = True
-            self._lastfm.now_playing(current_media)
+            self._lastfm.now_playing(current_song)
 
         duration = self._player.duration
         if duration is None:
@@ -575,7 +746,7 @@ class Player(GObject.GObject):
             if (not self._lastfm.scrobbled
                     and duration > 30
                     and (percentage > 0.5 or tick > 4 * 60)):
-                self._lastfm.scrobble(current_media, self._time_stamp)
+                self._lastfm.scrobble(current_song, self._time_stamp)
 
             if (percentage > 0.5
                     and self._new_clock):
@@ -584,11 +755,50 @@ class Player(GObject.GObject):
                 # 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)
+                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._playlist.props.repeat_mode
+
+    @repeat_mode.setter
+    def repeat_mode(self, mode):
+        self.emit('repeat-mode-changed')
+        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
+
     # MPRIS
     @log
     def get_gst_player(self):
@@ -610,19 +820,10 @@ class Player(GObject.GObject):
         # 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):
@@ -652,12 +853,5 @@ class Player(GObject.GObject):
         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]
+    def get_songs(self):
+        return self._playlist.get_songs()
diff --git a/gnomemusic/views/artistsview.py b/gnomemusic/views/artistsview.py
index 4581ec61..b9f0788f 100644
--- a/gnomemusic/views/artistsview.py
+++ b/gnomemusic/views/artistsview.py
@@ -28,6 +28,7 @@ from gi.repository import Gdk, GLib, Gtk
 
 from gnomemusic import log
 from gnomemusic.grilo import grilo
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.views.baseview import BaseView
 from gnomemusic.widgets.artistalbumswidget import ArtistAlbumsWidget
 from gnomemusic.widgets.sidebarrow import SidebarRow
@@ -108,7 +109,8 @@ class ArtistsView(BaseView):
         widget = self._artists[artist.casefold()]['widget']
 
         if widget:
-            if self.player.playing_playlist('Artist', widget.props.artist):
+            if self.player.playing_playlist(
+                    PlayerPlaylist.Type.ARTIST, widget.artist):
                 self._artist_albums_widget = widget.get_parent()
                 GLib.idle_add(
                     self._view.set_visible_child, self._artist_albums_widget)
diff --git a/gnomemusic/views/playlistview.py b/gnomemusic/views/playlistview.py
index 6d786c67..b9d0d088 100644
--- a/gnomemusic/views/playlistview.py
+++ b/gnomemusic/views/playlistview.py
@@ -28,7 +28,7 @@ from gi.repository import Gio, GLib, GObject, Gtk, Pango
 
 from gnomemusic import log
 from gnomemusic.grilo import grilo
-from gnomemusic.player import DiscoveryStatus
+from gnomemusic.player import ValidationStatus, PlayerPlaylist
 from gnomemusic.playlists import Playlists, StaticPlaylists
 from gnomemusic.views.baseview import BaseView
 from gnomemusic.widgets.notificationspopup import PlaylistNotification
@@ -131,6 +131,7 @@ class PlaylistView(BaseView):
         self.model.connect('row-deleted', self._on_song_deleted)
 
         self.player.connect('song-changed', self._update_model)
+        self.player.connect('song-validated', self._on_song_validated)
         playlists.connect('playlist-created', self._on_playlist_created)
         playlists.connect('playlist-updated', self._on_playlist_update)
         playlists.connect(
@@ -226,14 +227,14 @@ class PlaylistView(BaseView):
 
     def _on_list_widget_icon_render(self, col, cell, model, _iter, data):
         if not self.player.playing_playlist(
-                'Playlist', self._current_playlist.get_id()):
+                PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
             cell.set_visible(False)
             return
 
         if not model.iter_is_valid(_iter):
             return
 
-        if model[_iter][11] == DiscoveryStatus.FAILED:
+        if model[_iter][11] == ValidationStatus.FAILED:
             cell.set_property('icon-name', self._error_icon_name)
             cell.set_visible(True)
         elif model[_iter][5].get_url() == self.player.url:
@@ -243,15 +244,19 @@ class PlaylistView(BaseView):
             cell.set_visible(False)
 
     @log
-    def _update_model(self, player, playlist, current_iter):
+    def _update_model(self, player, position):
+        """Updates model when the song changes
+
+        :param Player player: The main player object
+        :param int position: current song position
+        """
         if self._iter_to_clean:
             self._iter_to_clean_model[self._iter_to_clean][10] = False
         if not player.playing_playlist(
-                'Playlist', self._current_playlist.get_id()):
+                PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
             return False
 
-        pos_str = playlist.get_path(current_iter).to_string()
-        iter_ = self.model.get_iter_from_string(pos_str)
+        iter_ = self.model.get_iter_from_string(str(position))
         self.model[iter_][10] = True
         if self.model[iter_][8] != self._error_icon_name:
             self._iter_to_clean = iter_.copy()
@@ -304,6 +309,15 @@ class PlaylistView(BaseView):
             self._sidebar.select_row(row)
             self._sidebar.emit('row-activated', row)
 
+    @log
+    def _on_song_validated(self, player, index, status):
+        if not self.player.playing_playlist(
+                PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
+            return
+
+        iter_ = self.model.get_iter_from_string(str(index))
+        self.model[iter_][11] = status
+
     @log
     def _on_song_activated(self, widget, path, column):
         """Action performed when clicking on a song
@@ -327,8 +341,8 @@ class PlaylistView(BaseView):
             _iter = self.model.get_iter(path)
             if self.model[_iter][8] != self._error_icon_name:
                 self.player.set_playlist(
-                    'Playlist', self._current_playlist.get_id(), self.model,
-                    _iter)
+                    PlayerPlaylist.Type.PLAYLIST,
+                    self._current_playlist.get_id(), self.model, _iter)
                 self.player.play()
 
         # 'row-activated' signal is emitted before 'drag-begin' signal.
@@ -376,7 +390,7 @@ class PlaylistView(BaseView):
     def _on_song_deleted(self, model, path):
         """Save new playlist order after drag and drop operation.
 
-        Update player's playlist if necessary.
+        Update player's playlist if the playlist is being played.
         """
         if not self._song_drag['active']:
             return
@@ -390,24 +404,21 @@ class PlaylistView(BaseView):
         first_pos = min(new_pos, prev_pos)
         last_pos = max(new_pos, prev_pos)
 
-        # update player's playlist.
+        # update player's playlist if necessary
         if self.player.playing_playlist(
-                'Playlist', self._current_playlist.get_id()):
-            playing_old_path = self.player.current_song.get_path().to_string()
-            playing_old_pos = int(playing_old_path)
-            iter_ = model.get_iter_from_string(playing_old_path)
-            # if playing song position has changed
-            if playing_old_pos >= first_pos and playing_old_pos < last_pos:
-                current_player_song = self.player.get_current_media()
-                for row in model:
-                    if row[5].get_id() == current_player_song.get_id():
-                        iter_ = row.iter
-                        self._iter_to_clean = iter_
-                        self._iter_to_clean_model = model
-                        break
-            self.player.set_playlist(
-                'Playlist', self._current_playlist.get_id(), model, iter_)
-
+                PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
+            if new_pos < prev_pos:
+                prev_pos -= 1
+            else:
+                new_pos -= 1
+            current_index = self.player.playlist_change_position(
+                prev_pos, new_pos)
+            if current_index >= 0:
+                current_iter = model.get_iter_from_string(str(current_index))
+                self._iter_to_clean = current_iter
+                self._iter_to_clean_model = model
+
+        # update playlist's storage
         positions = []
         songs = []
         for pos in range(first_pos, last_pos):
@@ -658,7 +669,8 @@ class PlaylistView(BaseView):
                     or self._sidebar.get_row_at_index(index - 1))
         self._sidebar.remove(selection)
 
-        if self.player.playing_playlist('Playlist', playlist_id):
+        if self.player.playing_playlist(
+                PlayerPlaylist.Type.PLAYLIST, playlist_id):
             self.player.stop()
             self.set_player_visible(False)
 
@@ -688,10 +700,12 @@ class PlaylistView(BaseView):
                     and playlist.get_id() == self._current_playlist.get_id()):
                 iter_ = self._add_song_to_model(
                     song_todelete['song'], self.model, song_todelete['index'])
+                playlist_id = self._current_playlist.get_id()
                 if self.player.playing_playlist(
-                        'Playlist', self._current_playlist.get_id()):
+                        PlayerPlaylist.Type.PLAYLIST, playlist_id):
+                    song = self.model[iter_][5]
                     path = self.model.get_path(iter_)
-                    self.player.add_song(self.model, path, iter_)
+                    self.player.add_song(song, int(path.to_string()))
             self._songs_todelete.pop(media_id)
 
     @log
@@ -739,23 +753,23 @@ class PlaylistView(BaseView):
     def _on_song_added_to_playlist(self, playlists, playlist, item):
         if self._is_current_playlist(playlist):
             iter_ = self._add_song_to_model(item, self.model)
+            playlist_id = self._current_playlist.get_id()
             if self.player.playing_playlist(
-                    'Playlist', self._current_playlist.get_id()):
+                    PlayerPlaylist.Type.PLAYLIST, playlist_id):
                 path = self.model.get_path(iter_)
-                self.player.add_song(self.model, path, iter_)
+                self.player.add_song(item, int(path.to_string()))
 
     @log
     def _remove_song_from_playlist(self, playlist, item, index):
         if not self._is_current_playlist(playlist):
             return
 
-        model = self.model
-
-        iter_ = model.get_iter_from_string(str(index))
         if self.player.playing_playlist(
-                'Playlist', self._current_playlist.get_id()):
-            self.player.remove_song(model, model.get_path(iter_))
-        model.remove(iter_)
+                PlayerPlaylist.Type.PLAYLIST, self._current_playlist.get_id()):
+            self.player.remove_song(index)
+
+        iter_ = self.model.get_iter_from_string(str(index))
+        self.model.remove(iter_)
 
         self._update_songs_count(self._songs_count - 1)
 
diff --git a/gnomemusic/views/searchview.py b/gnomemusic/views/searchview.py
index d93d2469..06b21f2b 100644
--- a/gnomemusic/views/searchview.py
+++ b/gnomemusic/views/searchview.py
@@ -28,7 +28,7 @@ from gi.repository import Gd, Gdk, GdkPixbuf, GObject, Grl, Gtk, Pango
 from gnomemusic.albumartcache import Art
 from gnomemusic.grilo import grilo
 from gnomemusic import log
-from gnomemusic.player import DiscoveryStatus
+from gnomemusic.player import ValidationStatus, PlayerPlaylist
 from gnomemusic.playlists import Playlists
 from gnomemusic.query import Query
 from gnomemusic.utils import View
@@ -157,10 +157,11 @@ class SearchView(BaseView):
             self.set_visible_child(self._artist_albums_widget)
             self._header_bar.searchbar.reveal(False)
         elif self.model[_iter][12] == 'song':
-            if self.model[_iter][11] != DiscoveryStatus.FAILED:
+            if self.model[_iter][11] != ValidationStatus.FAILED:
                 c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1]
                 self.player.set_playlist(
-                    'Search Results', None, self._songs_model, c_iter)
+                    PlayerPlaylist.Type.SEARCH_RESULT, None, self._songs_model,
+                    c_iter)
                 self.player.play()
         else:  # Headers
             if self._view.row_expanded(path):
@@ -554,7 +555,7 @@ class SearchView(BaseView):
             GObject.TYPE_STRING,
             GObject.TYPE_INT,
             GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT,       # discovery status
+            GObject.TYPE_INT,       # validation status
             GObject.TYPE_STRING,    # type
             object                  # album art surface
         )
diff --git a/gnomemusic/views/songsview.py b/gnomemusic/views/songsview.py
index 2292ff2e..e33bae55 100644
--- a/gnomemusic/views/songsview.py
+++ b/gnomemusic/views/songsview.py
@@ -28,7 +28,7 @@ from gi.repository import Gdk, GLib, Gtk, Pango
 
 from gnomemusic import log
 from gnomemusic.grilo import grilo
-from gnomemusic.player import DiscoveryStatus
+from gnomemusic.player import ValidationStatus, PlayerPlaylist
 from gnomemusic.views.baseview import BaseView
 import gnomemusic.utils as utils
 
@@ -63,6 +63,7 @@ class SongsView(BaseView):
 
         self.player = player
         self.player.connect('song-changed', self._update_model)
+        self.player.connect('song-validated', self._on_song_validated)
 
     @log
     def _setup_view(self):
@@ -154,7 +155,7 @@ class SongsView(BaseView):
             cell.props.visible = False
             return
 
-        if model[itr][11] == DiscoveryStatus.FAILED:
+        if model[itr][11] == ValidationStatus.FAILED:
             cell.props.icon_name = self._error_icon_name
             cell.props.visible = True
         elif model[itr][5].get_url() == track_uri:
@@ -202,9 +203,9 @@ class SongsView(BaseView):
             return
 
         itr = self.model.get_iter(path)
-        if self.model[itr][8] != self._error_icon_name:
-            self.player.set_playlist('Songs', None, self.model, itr)
-            self.player.play()
+        self.player.set_playlist(
+            PlayerPlaylist.Type.SONGS, None, self.model, itr)
+        self.player.play()
 
     @log
     def _on_view_clicked(self, treeview, event):
@@ -227,20 +228,18 @@ class SongsView(BaseView):
             self._update_header_from_selection(len(self.get_selected_songs()))
 
     @log
-    def _update_model(self, player, playlist, current_iter):
-        """Updates model when the track changes
+    def _update_model(self, player, position):
+        """Updates model when the song changes
 
-        :param player: The main player object
-        :param playlist: The current playlist object
-        :param current_iter: Iter of the current displayed song
+        :param Player player: The main player object
+        :param int position: current song position
         """
         if self._iter_to_clean:
             self.model[self._iter_to_clean][10] = False
-        if not player.playing_playlist('Songs', None):
+        if not player.playing_playlist(PlayerPlaylist.Type.SONGS, None):
             return False
 
-        pos_str = playlist.get_path(current_iter).to_string()
-        iter_ = self.model.get_iter_from_string(pos_str)
+        iter_ = self.model.get_iter_from_string(str(position))
         self.model[iter_][10] = True
         path = self.model.get_path(iter_)
         self._view.scroll_to_cell(path, None, False, 0., 0.)
@@ -248,6 +247,14 @@ class SongsView(BaseView):
             self._iter_to_clean = iter_.copy()
         return False
 
+    @log
+    def _on_song_validated(self, player, index, status):
+        if not player.playing_playlist(PlayerPlaylist.Type.SONGS, None):
+            return
+
+        iter_ = self.model.get_iter_from_string(str(index))
+        self.model[iter_][11] = status
+
     def _add_item(self, source, param, item, remaining=0, data=None):
         """Adds track item to the model"""
         if not item and not remaining:
diff --git a/gnomemusic/widgets/albumwidget.py b/gnomemusic/widgets/albumwidget.py
index 2e2b75fa..d0433d4f 100644
--- a/gnomemusic/widgets/albumwidget.py
+++ b/gnomemusic/widgets/albumwidget.py
@@ -28,6 +28,7 @@ from gi.repository import GdkPixbuf, GLib, GObject, Gtk
 from gnomemusic import log
 from gnomemusic.albumartcache import Art, ArtImage
 from gnomemusic.grilo import grilo
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.widgets.disclistboxwidget import DiscBox
 from gnomemusic.widgets.disclistboxwidget import DiscListBox  # noqa: F401
 from gnomemusic.widgets.songwidget import SongWidget
@@ -186,9 +187,9 @@ class AlbumWidget(Gtk.EventBox):
             song_widget.props.selected = not song_widget.props.selected
             return
 
-        self._player.stop()
         self._player.set_playlist(
-            'Album', self._album, song_widget.model, song_widget.itr)
+            PlayerPlaylist.Type.ALBUM, self._album, song_widget.model,
+            song_widget.itr)
         self._player.play()
         return True
 
@@ -227,29 +228,24 @@ class AlbumWidget(Gtk.EventBox):
             self.show_all()
 
     @log
-    def _update_model(self, player, playlist, current_iter):
-        """Player changed callback.
+    def _update_model(self, player, position):
+        """Updates model when the song changes
 
-        :param player: The player object
-        :param playlist: The current playlist
-        :param current_iter: The current iter of the playlist model
+        :param Player player: The main player object
+        :param int position: current song position
         """
-        if not player.playing_playlist('Album', self._album):
+        if not player.playing_playlist(PlayerPlaylist.Type.ALBUM, self._album):
             return True
 
-        current_song = playlist[current_iter][player.Field.SONG]
-
+        current_song = player.props.current_song
         self._duration = 0
-
         song_passed = False
-        _iter = playlist.get_iter_first()
 
-        while _iter:
-            song = playlist[_iter][player.Field.SONG]
+        for song in self._songs:
             song_widget = song.song_widget
             self._duration += song.get_duration()
 
-            if (song == current_song):
+            if (song.get_id() == current_song.get_id()):
                 song_widget.props.state = SongWidget.State.PLAYING
                 song_passed = True
             elif (song_passed):
@@ -258,8 +254,6 @@ class AlbumWidget(Gtk.EventBox):
             else:
                 song_widget.props.state = SongWidget.State.PLAYED
 
-            _iter = playlist.iter_next(_iter)
-
         self._set_duration_label()
 
         return True
diff --git a/gnomemusic/widgets/artistalbumswidget.py b/gnomemusic/widgets/artistalbumswidget.py
index 2e629465..cba935f5 100644
--- a/gnomemusic/widgets/artistalbumswidget.py
+++ b/gnomemusic/widgets/artistalbumswidget.py
@@ -27,6 +27,7 @@ import logging
 from gi.repository import GObject, Gtk
 
 from gnomemusic import log
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.widgets.artistalbumwidget import ArtistAlbumWidget
 from gnomemusic.widgets.songwidget import SongWidget
 
@@ -143,20 +144,26 @@ class ArtistAlbumsWidget(Gtk.Box):
         widget.connect('songs-loaded', self._on_album_displayed)
 
     @log
-    def _update_model(self, player, playlist, current_iter):
-        if not player.playing_playlist('Artist', self.props.artist):
+    def _update_model(self, player, position):
+        """Updates model when the song changes
+
+        :param Player player: The main player object
+        :param int position: current song position
+        """
+        if not player.playing_playlist(
+                PlayerPlaylist.Type.ARTIST, self._artist):
             self._clean_model()
             return False
 
-        current_song = playlist[current_iter][player.Field.SONG]
+        current_song = player.props.current_song
         song_passed = False
-        itr = playlist.get_iter_first()
+        itr = self._model.get_iter_first()
 
         while itr:
-            song = playlist[itr][player.Field.SONG]
+            song = self._model[itr][5]
             song_widget = song.song_widget
 
-            if (song == current_song):
+            if (song.get_id() == current_song.get_id()):
                 song_widget.props.state = SongWidget.State.PLAYING
                 song_passed = True
             elif (song_passed):
@@ -165,7 +172,7 @@ class ArtistAlbumsWidget(Gtk.Box):
             else:
                 song_widget.props.state = SongWidget.State.PLAYED
 
-            itr = playlist.iter_next(itr)
+            itr = self._model.iter_next(itr)
 
         return False
 
diff --git a/gnomemusic/widgets/artistalbumwidget.py b/gnomemusic/widgets/artistalbumwidget.py
index d852b789..d5e854d8 100644
--- a/gnomemusic/widgets/artistalbumwidget.py
+++ b/gnomemusic/widgets/artistalbumwidget.py
@@ -27,6 +27,7 @@ from gi.repository import GObject, Gtk
 from gnomemusic import log
 from gnomemusic.albumartcache import Art
 from gnomemusic.grilo import grilo
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.widgets.disclistboxwidget import DiscBox
 import gnomemusic.utils as utils
 
@@ -137,9 +138,9 @@ class ArtistAlbumWidget(Gtk.Box):
         if self.props.selection_mode:
             return
 
-        self._player.stop()
         self._player.set_playlist(
-            'Artist', self._artist, song_widget.model, song_widget.itr)
+            PlayerPlaylist.Type.ARTIST, self._artist, song_widget.model,
+            song_widget.itr)
         self._player.play()
 
         return True
diff --git a/gnomemusic/widgets/playertoolbar.py b/gnomemusic/widgets/playertoolbar.py
index ad99d9b4..63d86b28 100644
--- a/gnomemusic/widgets/playertoolbar.py
+++ b/gnomemusic/widgets/playertoolbar.py
@@ -127,13 +127,13 @@ class PlayerToolbar(Gtk.ActionBar):
     @log
     def _sync_repeat_image(self, player=None):
         icon = None
-        if self._player.repeat == RepeatMode.NONE:
+        if self._player.props.repeat_mode == RepeatMode.NONE:
             icon = 'media-playlist-consecutive-symbolic'
-        elif self._player.repeat == RepeatMode.SHUFFLE:
+        elif self._player.props.repeat_mode == RepeatMode.SHUFFLE:
             icon = 'media-playlist-shuffle-symbolic'
-        elif self._player.repeat == RepeatMode.ALL:
+        elif self._player.props.repeat_mode == RepeatMode.ALL:
             icon = 'media-playlist-repeat-symbolic'
-        elif self._player.repeat == RepeatMode.SONG:
+        elif self._player.props.repeat_mode == RepeatMode.SONG:
             icon = 'media-playlist-repeat-song-symbolic'
 
         self._repeat_image.set_from_icon_name(icon, Gtk.IconSize.MENU)
@@ -157,21 +157,26 @@ class PlayerToolbar(Gtk.ActionBar):
 
     @log
     def _sync_prev_next(self, player=None):
-        self._next_button.set_sensitive(self._player.has_next())
-        self._prev_button.set_sensitive(self._player.has_previous())
+        self._next_button.props.sensitive = self._player.props.has_next
+        self._prev_button.props.sensitive = self._player.props.has_previous
 
     @log
-    def _update_view(self, player, playlist, current_iter):
-        media = playlist[current_iter][player.Field.SONG]
+    def _update_view(self, player, position):
+        """Updates model when the song changes
+
+        :param Player player: The main player object
+        :param int position: current song position
+        """
+        current_song = player.props.current_song
         self._duration_label.set_label(
-            utils.seconds_to_string(media.get_duration()))
+            utils.seconds_to_string(current_song.get_duration()))
 
         self._play_button.set_sensitive(True)
         self._sync_prev_next()
 
-        self._artist_label.set_label(utils.get_artist_name(media))
-        self._title_label.set_label(utils.get_media_title(media))
-        self._cover_stack.update(media)
+        self._artist_label.set_label(utils.get_artist_name(current_song))
+        self._title_label.set_label(utils.get_media_title(current_song))
+        self._cover_stack.update(current_song)
 
     @log
     def _on_clock_tick(self, player, seconds):
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index 625bec21..b46d252c 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -304,16 +304,16 @@ class Window(Gtk.ApplicationWindow):
                 self.player.next()
             # Toggle repeat on Ctrl + R
             if keyval == Gdk.KEY_r:
-                if self.player.get_repeat_mode() == RepeatMode.SONG:
-                    self.player.set_repeat_mode(RepeatMode.NONE)
+                if self.player.props.repeat_mode == RepeatMode.SONG:
+                    self.player.props.repeat_mode = RepeatMode.NONE
                 else:
-                    self.player.set_repeat_mode(RepeatMode.SONG)
+                    self.player.props.repeat_mode = RepeatMode.SONG
             # Toggle shuffle on Ctrl + S
             if keyval == Gdk.KEY_s:
-                if self.player.get_repeat_mode() == RepeatMode.SHUFFLE:
-                    self.player.set_repeat_mode(RepeatMode.NONE)
+                if self.player.props.repeat_mode == RepeatMode.SHUFFLE:
+                    self.player.props.repeat_mode = RepeatMode.NONE
                 else:
-                    self.player.set_repeat_mode(RepeatMode.SHUFFLE)
+                    self.player.props.repeat_mode = RepeatMode.SHUFFLE
             # Headerbar switching
             if keyval in [Gdk.KEY_1, Gdk.KEY_KP_1]:
                 self._toggle_view(View.ALBUM)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]