[gnome-music/wip/mschraal/core-squash] Complete rewrite of the core of Music



commit e626a26ad0912e2b322a09638ef1fa95bceb117d
Author: Marinus Schraal <mschraal gnome org>
Date:   Mon Jul 15 23:41:26 2019 +0200

    Complete rewrite of the core of Music
    
    Use persistent list models throughout Music to hold the state of a
    users music collected through Grilo and Tracker.
    
    The extent of the rework did not allow for a gradual approach, so this
    commit marks a hard break from the old approach. It is not yet on-par
    with the old codebase.
    
    This rework is partially based on the work of
    Jean Felder <jfelder src gnome org>.
    
    Related: #299.

 .gitlab-ci.yml                                     |   2 +-
 .gitmodules                                        |   3 +
 data/org.gnome.Music.css                           |  57 +-
 data/org.gnome.Music.gresource.xml                 |   4 +-
 data/ui/AlbumWidget.ui                             |  12 +-
 data/ui/ArtistAlbumWidget.ui                       |  36 +-
 data/ui/ArtistAlbumsWidget.ui                      |  37 -
 data/ui/{SidebarRow.ui => ArtistTile.ui}           |   4 +-
 data/ui/DiscBox.ui                                 |  16 +-
 data/ui/PlaylistControls.ui                        |   8 +
 data/ui/PlaylistTile.ui                            |  16 +
 data/ui/SongWidget.ui                              | 100 ++-
 gnome-music.in                                     |  11 +
 gnomemusic/albumartcache.py                        | 119 ++-
 gnomemusic/application.py                          |  26 +-
 gnomemusic/corealbum.py                            |  99 ++
 gnomemusic/coreartist.py                           |  84 ++
 gnomemusic/coredisc.py                             | 141 +++
 gnomemusic/coregrilo.py                            | 207 +++++
 gnomemusic/coremodel.py                            | 456 ++++++++++
 gnomemusic/coreselection.py                        |  50 ++
 gnomemusic/coresong.py                             | 120 +++
 gnomemusic/grilo.py                                | 525 -----------
 gnomemusic/grilowrappers/__init__.py               |   0
 gnomemusic/grilowrappers/grldleynawrapper.py       | 121 +++
 gnomemusic/grilowrappers/grlsearchwrapper.py       |  95 ++
 gnomemusic/grilowrappers/grltrackerplaylists.py    | 851 ++++++++++++++++++
 gnomemusic/grilowrappers/grltrackerwrapper.py      | 878 ++++++++++++++++++
 gnomemusic/gstplayer.py                            |   3 +-
 gnomemusic/mpris.py                                | 146 +--
 gnomemusic/player.py                               | 646 +++++--------
 gnomemusic/playlists.py                            | 628 -------------
 gnomemusic/query.py                                | 996 ---------------------
 gnomemusic/scrobbler.py                            |  10 +-
 gnomemusic/songliststore.py                        |  91 ++
 gnomemusic/utils.py                                |   7 +-
 gnomemusic/views/albumsview.py                     | 136 +--
 gnomemusic/views/artistsview.py                    | 225 +++--
 gnomemusic/views/baseview.py                       |  58 +-
 gnomemusic/views/emptyview.py                      |  17 +-
 gnomemusic/views/playlistsview.py                  | 658 +++-----------
 gnomemusic/views/searchview.py                     | 662 ++++----------
 gnomemusic/views/songsview.py                      | 166 ++--
 gnomemusic/widgets/albumcover.py                   |  30 +-
 gnomemusic/widgets/albumwidget.py                  | 248 ++---
 gnomemusic/widgets/artistalbumswidget.py           | 166 +---
 gnomemusic/widgets/artistalbumwidget.py            | 105 +--
 .../widgets/{sidebarrow.py => artisttile.py}       |  20 +-
 gnomemusic/widgets/coverstack.py                   |  10 +-
 gnomemusic/widgets/disclistboxwidget.py            | 187 +---
 gnomemusic/widgets/notificationspopup.py           |  69 +-
 gnomemusic/widgets/playertoolbar.py                |  12 +-
 gnomemusic/widgets/playlistcontrols.py             |  51 +-
 gnomemusic/widgets/playlistdialog.py               |  30 +-
 gnomemusic/widgets/playlistdialogrow.py            |   2 +-
 gnomemusic/widgets/playlisttile.py                 |  58 ++
 gnomemusic/widgets/searchbar.py                    |  31 +-
 gnomemusic/widgets/selectiontoolbar.py             |   2 +
 gnomemusic/widgets/songwidget.py                   | 124 ++-
 gnomemusic/widgets/starhandlerwidget.py            |  19 +-
 gnomemusic/window.py                               |  82 +-
 meson.build                                        |  14 +-
 org.gnome.Music.json                               |   5 +-
 po/POTFILES.in                                     |   4 +-
 subprojects/gfm                                    |   1 +
 65 files changed, 4948 insertions(+), 4849 deletions(-)
---
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c25e3c0b..fa7e6cb9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -63,4 +63,4 @@ flake8:
   stage: check
   script:
     - dnf install -y python3-flake8
-    - flake8 --ignore E402,W503 --show-source --exclude=grilo.py,query.py gnomemusic/
+    - flake8 --ignore E402,W503 --show-source gnomemusic/
diff --git a/.gitmodules b/.gitmodules
index b2aeb1fe..9b66516e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "subprojects/shared-modules"]
        path = subprojects/shared-modules
        url = https://github.com/flathub/shared-modules.git
+[submodule "subprojects/gfm"]
+       path = subprojects/gfm
+       url = https://gitlab.gnome.org/mschraal/gfm.git
diff --git a/data/org.gnome.Music.css b/data/org.gnome.Music.css
index 6caaa519..dd4e166e 100644
--- a/data/org.gnome.Music.css
+++ b/data/org.gnome.Music.css
@@ -18,6 +18,12 @@
     font-weight: bold;
 }
 
+.disc-label {
+    background-color: @theme_bg_color;
+    color: alpha(@theme_fg_color, 0.8);
+    padding: 12px 0;
+}
+
 /* ArtistAlbumsWidget */
 box#ArtistAlbumsWidget .artist-label {
     font-weight: bold;
@@ -46,17 +52,28 @@ box#ArtistAlbumsWidget .artist-label {
 }
 
 /* ArtistAlbumWidget */
+.artist-albums-widget,
+.artist-albums-widget:hover {
+    background-color: @theme_bg_color;
+}
+
+.artist-albums-widget > row:hover {
+    background-color: rgba(0, 0, 0, 0.0);
+    box-shadow: none;
+}
+
 .album-title {
-    padding-left:24px;
+    font-size: large;
     font-weight: bold;
 }
 
-.songs-list {
+/* FIXME: Remove once songsview is ported to the new style */
+.songs-list-old {
     box-shadow: inset 0 -1px shade(@borders, 1.30);
     background-color: @theme_bg_color;
 }
 
-.songs-list:selected {
+.songs-list-old:selected {
     color: @theme_fg_color;
     border-color: mix(@theme_fg_color, @theme_bg_color, 0.5);
 }
@@ -95,11 +112,41 @@ box#ArtistAlbumsWidget .artist-label {
   font-weight: bold;
 }
 
-/* PlaylistDialog */
-.playlistdialog-row {
+/* Lists style */
+
+/* workaround to avoid a black background issue
+in AlbumWidget and PlaylistsView
+https://gitlab.gnome.org/GNOME/gtk/issues/694
+*/
+list {
+    background-color: transparent;
+}
+
+/* workaround to avoid a black background issue
+in AlbumWidget and PlaylistsView
+https://gitlab.gnome.org/GNOME/gtk/issues/694 */
+.songs-list > row {
+    background-color: @theme_base_color;
+}
+
+
+
+.disc-list-box > row {
+padding: 0px;
+}
+
+.songs-list {
+    border: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.songs-list > row {
     border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 }
 
+.songs-list > row:last-child {
+    border-bottom: none;
+}
+
 .playlistdialog-row:selected {
     color: @theme_fg_color;
     background-color: @theme_insensitive_bg_color;
diff --git a/data/org.gnome.Music.gresource.xml b/data/org.gnome.Music.gresource.xml
index 3992ffa4..7b64dd1f 100644
--- a/data/org.gnome.Music.gresource.xml
+++ b/data/org.gnome.Music.gresource.xml
@@ -9,7 +9,7 @@
     <file preprocess="xml-stripblanks">ui/AlbumWidget.ui</file>
     <file preprocess="xml-stripblanks">ui/AppMenu.ui</file>
     <file preprocess="xml-stripblanks">ui/ArtistAlbumWidget.ui</file>
-    <file preprocess="xml-stripblanks">ui/ArtistAlbumsWidget.ui</file>
+    <file preprocess="xml-stripblanks">ui/ArtistTile.ui</file>
     <file preprocess="xml-stripblanks">ui/DiscBox.ui</file>
     <file preprocess="xml-stripblanks">ui/DropDown.ui</file>
     <file preprocess="xml-stripblanks">ui/EmptyView.ui</file>
@@ -20,10 +20,10 @@
     <file preprocess="xml-stripblanks">ui/PlaylistControls.ui</file>
     <file preprocess="xml-stripblanks">ui/PlaylistDialog.ui</file>
     <file preprocess="xml-stripblanks">ui/PlaylistDialogRow.ui</file>
+    <file preprocess="xml-stripblanks">ui/PlaylistTile.ui</file>
     <file preprocess="xml-stripblanks">ui/SearchBar.ui</file>
     <file preprocess="xml-stripblanks">ui/SelectionBarMenuButton.ui</file>
     <file preprocess="xml-stripblanks">ui/SelectionToolbar.ui</file>
-    <file preprocess="xml-stripblanks">ui/SidebarRow.ui</file>
     <file preprocess="xml-stripblanks">ui/SongWidget.ui</file>
     <file preprocess="xml-stripblanks">ui/TwoLineTip.ui</file>
     <file preprocess="xml-stripblanks">ui/Window.ui</file>
diff --git a/data/ui/AlbumWidget.ui b/data/ui/AlbumWidget.ui
index 04f70b03..7f6b770d 100644
--- a/data/ui/AlbumWidget.ui
+++ b/data/ui/AlbumWidget.ui
@@ -220,19 +220,13 @@
                 <property name="hexpand">True</property>
                 <property name="shadow_type">none</property>
                 <child>
-                  <!-- TODO: The top of the coverart is the same vertical -->
-                  <!-- position as the top of the album songs, however -->
-                  <!-- since we set a top margins for the discbox -->
-                  <!-- subtract that margin here. A cleaner solution is appreciated. -->
-                  <object class="DiscListBox" id="_disc_listbox">
+                  <object class="DiscListBox" id="_listbox">
                     <property name="can_focus">False</property>
-                    <property name="margin_top">48</property>
+                    <property name="margin_top">64</property>
                     <property name="margin_bottom">64</property>
                     <property name="margin_end">32</property>
-                    <property name="orientation">vertical</property>
-                    <property name="selection_mode_allowed">True</property>
+                    <property name="selection_mode">0</property>
                     <property name="visible">True</property>
-                    <signal name="selection-changed" handler="_on_selection_changed" swapped="no"/>
                   </object>
                 </child>
               </object>
diff --git a/data/ui/ArtistAlbumWidget.ui b/data/ui/ArtistAlbumWidget.ui
index 6f92a7b4..a5ca2d2e 100644
--- a/data/ui/ArtistAlbumWidget.ui
+++ b/data/ui/ArtistAlbumWidget.ui
@@ -3,14 +3,17 @@
 <interface>
   <requires lib="gtk+" version="3.12"/>
   <template parent="GtkBox" class="ArtistAlbumWidget">
+    <property name="margin_top">30</property>
+    <property name="margin_right">120</property>
     <child>
       <object class="CoverStack" id="_cover_stack">
         <property name="visible">True</property>
+       <property name="margin_top">20</property>
+       <property name="margin_right">30</property>
+        <property name="margin_bottom">20</property>
+       <property name="margin_left">120</property>
         <property name="can_focus">False</property>
         <property name="valign">start</property>
-        <style>
-          <class name="album-cover"/>
-        </style>
       </object>
       <packing>
         <property name="position">0</property>
@@ -26,36 +29,18 @@
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <child>
-              <object class="GtkLabel" id="_title">
+              <object class="GtkLabel" id="_title_year">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="margin_start">6</property>
-                <property name="margin_end">6</property>
+                <property name="margin_top">20</property>
+                <property name="margin_bottom">20</property>
                 <property name="ellipsize">middle</property>
                 <property name="xalign">0</property>
                 <property name="yalign">0</property>
                 <style>
                   <class name="album-title"/>
-                  <class name="dim-label"/>
                 </style>
               </object>
-              <packing>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkLabel" id="_year">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="xalign">0</property>
-                <property name="yalign">0</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
-              </object>
-              <packing>
-                <property name="position">1</property>
-              </packing>
             </child>
           </object>
           <packing>
@@ -66,7 +51,8 @@
           <object class="DiscListBox" id="_disc_list_box">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="orientation">vertical</property>
+            <property name="selection-mode">0</property>
+            <!-- <property name="orientation">vertical</property> -->
           </object>
           <packing>
             <property name="position">1</property>
diff --git a/data/ui/SidebarRow.ui b/data/ui/ArtistTile.ui
similarity index 90%
rename from data/ui/SidebarRow.ui
rename to data/ui/ArtistTile.ui
index eb89444b..6c75c483 100644
--- a/data/ui/SidebarRow.ui
+++ b/data/ui/ArtistTile.ui
@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <template class="SidebarRow" parent="GtkListBoxRow">
+  <template class="ArtistTile" parent="GtkEventBox">
+    <property name="can_focus">False</property>
+    <property name="visible">True</property>
     <child>
       <object class="GtkBox">
         <property name="can_focus">False</property>
diff --git a/data/ui/DiscBox.ui b/data/ui/DiscBox.ui
index 923f46e3..d1576f37 100644
--- a/data/ui/DiscBox.ui
+++ b/data/ui/DiscBox.ui
@@ -6,17 +6,16 @@
       <property name="visible">True</property>
       <property name="can_focus">False</property>
       <property name="orientation">vertical</property>
-      <property name="margin_top">16</property>
       <child>
         <object class="GtkLabel" id="_disc_label">
           <property name="visible">True</property>
           <property name="can_focus">False</property>
-          <property name="halign">start</property>
-          <property name="margin_start">60</property>
-          <property name="margin_bottom">4</property>
+          <property name="halign">fill</property>
+          <!-- <property name="hexpand">True</property> -->
           <property name="no_show_all">True</property>
+          <property name="xalign">0.0</property>
           <style>
-            <class name="dim-label"/>
+            <class name="disc-label"/>
           </style>
         </object>
         <packing>
@@ -24,13 +23,14 @@
         </packing>
       </child>
       <child>
-        <object class="DiscSongsFlowBox" id="_disc_songs_flowbox">
+        <object class="GtkListBox" id="_list_box">
           <property name="visible">True</property>
           <property name="can_focus">False</property>
           <property name="valign">start</property>
-          <property name="orientation">vertical</property>
-          <property name="homogeneous">True</property>
           <property name="selection_mode">none</property>
+          <style>
+            <class name="songs-list"/>
+          </style>
         </object>
         <packing>
           <property name="position">1</property>
diff --git a/data/ui/PlaylistControls.ui b/data/ui/PlaylistControls.ui
index 64456fdd..4f4c0e3d 100644
--- a/data/ui/PlaylistControls.ui
+++ b/data/ui/PlaylistControls.ui
@@ -15,6 +15,13 @@
       <attribute name="action">win.playlist_rename</attribute>
     </item>
   </menu>
+  <object class="GtkImage" id="_view_more_image">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_top">1</property>
+    <property name="icon_name">view-more-symbolic</property>
+    <property name="icon_size">1</property>
+  </object>
   <template class="PlaylistControls" parent="GtkGrid">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
@@ -115,6 +122,7 @@
         <property name="menu-model">playlistMenu</property>
         <property name="direction">none</property>
         <property name="use_popover">True</property>
+        <property name="image">_view_more_image</property>
         <style>
           <class name="image-button"/>
           <class name="circular"/>
diff --git a/data/ui/PlaylistTile.ui b/data/ui/PlaylistTile.ui
new file mode 100644
index 00000000..dceea3c9
--- /dev/null
+++ b/data/ui/PlaylistTile.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="PlaylistTile" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkLabel" id="_label">
+        <property name="can_focus">False</property>
+        <property name="ellipsize">end</property>
+        <property name="halign">start</property>
+        <property name="hexpand">False</property>
+        <property name="margin">16</property>
+        <property name="visible">True</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/SongWidget.ui b/data/ui/SongWidget.ui
index 28f4675a..31a8e2d6 100644
--- a/data/ui/SongWidget.ui
+++ b/data/ui/SongWidget.ui
@@ -6,11 +6,28 @@
     <property name="visible">True</property>
     <property name="can_focus">False</property>
     <signal name="notify::selected" handler="_on_selection_changed"/>
+    <signal name="drag_data_received" handler="_on_drag_data_received"/>
     <child>
       <object class="GtkBox" id="box1">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
         <property name="spacing">3</property>
+        <property name="margin_top">10</property>
+        <property name="margin_bottom">10</property>
+        <child>
+          <object class="GtkEventBox" id="_dnd_eventbox">
+            <property name="visible">False</property>
+            <signal name="drag-begin" handler="_on_drag_begin"/>
+            <signal name="drag-end" handler="_on_drag_end"/>
+            <signal name="drag_data_get" handler="_on_drag_data_get"/>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon-name">open-menu-symbolic</property>
+              </object>
+            </child>
+          </object>
+        </child>
         <child>
           <object class="GtkBox" id="box3">
             <property name="width_request">48</property>
@@ -28,9 +45,6 @@
                   </object>
                 </child>
               </object>
-              <packing>
-                <property name="position">0</property>
-              </packing>
             </child>
             <child>
               <object class="GtkCheckButton" id="_select_button">
@@ -40,9 +54,6 @@
                 <property name="no_show_all">True</property>
                 <property name="draw_indicator">True</property>
               </object>
-              <packing>
-                <property name="position">1</property>
-              </packing>
             </child>
             <child>
               <object class="GtkLabel" id="_number_label">
@@ -54,18 +65,11 @@
                   <class name="dim-label"/>
                 </style>
               </object>
-              <packing>
-                <property name="pack_type">end</property>
-                <property name="position">2</property>
-              </packing>
             </child>
           </object>
-          <packing>
-            <property name="position">0</property>
-          </packing>
         </child>
         <child>
-          <object class="GtkBox" id="box2">
+          <object class="GtkBox" id="title_box">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="margin_top">1</property>
@@ -84,9 +88,52 @@
                 <property name="justify">fill</property>
                 <property name="margin_start">9</property>
               </object>
-              <packing>
-                <property name="position">0</property>
-              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="_artist_box">
+            <property name="visible">False</property>
+            <property name="can_focus">False</property>
+            <property name="margin_top">1</property>
+            <property name="margin_bottom">1</property>
+            <property name="hexpand">True</property>
+            <child>
+              <object class="GtkLabel" id="_artist_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+                <property name="valign">start</property>
+                <property name="ellipsize">end</property>
+                <property name="max_width_chars">90</property>
+                <property name="justify">fill</property>
+                <property name="margin_start">9</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="_album_duration_box">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_top">1</property>
+            <property name="margin_bottom">1</property>
+            <property name="hexpand">True</property>
+            <child>
+              <object class="GtkLabel" id="_album_label">
+                <property name="visible">False</property>
+                <property name="can_focus">False</property>
+                <property name="xalign">0</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+                <property name="valign">start</property>
+                <property name="ellipsize">end</property>
+                <property name="max_width_chars">90</property>
+                <property name="justify">fill</property>
+                <property name="margin_start">9</property>
+              </object>
             </child>
             <child>
               <object class="GtkLabel" id="_duration_label">
@@ -95,16 +142,11 @@
                 <property name="no_show_all">True</property>
                 <property name="halign">end</property>
                 <property name="valign">center</property>
+                <property name="hexpand">True</property>
                 <property name="single_line_mode">True</property>
               </object>
-              <packing>
-                <property name="position">1</property>
-              </packing>
             </child>
           </object>
-          <packing>
-            <property name="position">1</property>
-          </packing>
         </child>
         <child>
           <object class="GtkEventBox" id="_star_eventbox">
@@ -114,6 +156,7 @@
             <property name="halign">end</property>
             <property name="valign">center</property>
             <property name="visible_window">True</property>
+            <property name="margin_right">12</property>
             <signal name="button-release-event" handler="_on_star_toggle" swapped="no"/>
             <signal name="enter-notify-event" handler="_on_star_hover" swapped="no"/>
             <signal name="leave-notify-event" handler="_on_star_unhover" swapped="no"/>
@@ -126,9 +169,6 @@
               </object>
             </child>
           </object>
-          <packing>
-            <property name="position">2</property>
-          </packing>
         </child>
         <child>
           <placeholder/>
@@ -136,4 +176,12 @@
       </object>
     </child>
   </template>
+  <object class="GtkSizeGroup" id="_size_group">
+    <property name="mode">horizontal</property>
+  <widgets>
+    <widget name="title_box"/>
+    <widget name="_artist_box"/>
+    <widget name="_album_duration_box"/>
+  </widgets>
+</object>
 </interface>
diff --git a/gnome-music.in b/gnome-music.in
index 13f8aeb1..fe0de970 100755
--- a/gnome-music.in
+++ b/gnome-music.in
@@ -70,6 +70,16 @@ def set_libgd():
     GIRepository.Repository.prepend_search_path(libgd_typelibdir)
     GIRepository.Repository.prepend_library_path(libgd_libdir)
 
+def set_gfm():
+    """Configures application to use gfm."""
+    gfm_libdir = '@gfmlibdir@'
+    if _LOCAL:
+        gfm_typelibdir = '@gfmlibdir@'
+    else:
+        gfm_typelibdir = '@gfmlibdir@/girepository-1.0'
+
+    GIRepository.Repository.prepend_search_path(gfm_typelibdir)
+    GIRepository.Repository.prepend_library_path(gfm_libdir)
 
 def set_exception_hook():
     """Configures sys.excepthook to enforce Gtk application exiting."""
@@ -130,6 +140,7 @@ def run_application():
 def main():
     """Sets environment and runs GNOME Music."""
     set_libgd()
+    set_gfm()
     set_exception_hook()
     set_log_level()
     set_internationalization()
diff --git a/gnomemusic/albumartcache.py b/gnomemusic/albumartcache.py
index e9f60826..ec7ca541 100644
--- a/gnomemusic/albumartcache.py
+++ b/gnomemusic/albumartcache.py
@@ -35,23 +35,24 @@ from gi.repository import (Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, MediaArt,
                            Gst, GstTag, GstPbutils)
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
-import gnomemusic.utils as utils
 
 
 logger = logging.getLogger(__name__)
 
 
 @log
-def lookup_art_file_from_cache(media):
+def lookup_art_file_from_cache(coresong):
     """Lookup MediaArt cache art of an album or song.
 
-    :param Grl.Media media: song or album
+    :param CoreSong coresong: song or album
     :returns: a cache file
     :rtype: Gio.File
     """
-    album = utils.get_album_title(media)
-    artist = utils.get_artist_name(media)
+    try:
+        album = coresong.props.album
+    except AttributeError:
+        album = coresong.props.title
+    artist = coresong.props.artist
 
     success, thumb_file = MediaArt.get_file(artist, album, "album")
     if (not success
@@ -198,12 +199,16 @@ class Art(GObject.GObject):
         return '<Art>'
 
     @log
-    def __init__(self, size, media, scale=1):
+    def __init__(self, size, coresong, scale=1):
         super().__init__()
 
         self._size = size
-        self._media = media
-        self._media_url = self._media.get_url()
+        self._coresong = coresong
+        # FIXME: Albums do not have a URL.
+        try:
+            self._url = self._coresong.props.url
+        except AttributeError:
+            self._url = None
         self._surface = None
         self._scale = scale
 
@@ -217,14 +222,14 @@ class Art(GObject.GObject):
         cache = Cache()
         cache.connect('miss', self._cache_miss)
         cache.connect('hit', self._cache_hit)
-        cache.query(self._media)
+        cache.query(self._coresong)
 
     @log
     def _cache_miss(self, klass):
         embedded_art = EmbeddedArt()
         embedded_art.connect('found', self._embedded_art_found)
         embedded_art.connect('unavailable', self._embedded_art_unavailable)
-        embedded_art.query(self._media)
+        embedded_art.query(self._coresong)
 
     @log
     def _cache_hit(self, klass, pixbuf):
@@ -246,7 +251,7 @@ class Art(GObject.GObject):
         # chance of getting artwork.
         cache.connect('miss', self._embedded_art_unavailable)
         cache.connect('hit', self._cache_hit)
-        cache.query(self._media)
+        cache.query(self._coresong)
 
     @log
     def _embedded_art_unavailable(self, klass):
@@ -254,14 +259,14 @@ class Art(GObject.GObject):
         remote_art.connect('retrieved', self._remote_art_retrieved)
         remote_art.connect('unavailable', self._remote_art_unavailable)
         remote_art.connect('no-remote-sources', self._remote_art_no_sources)
-        remote_art.query(self._media)
+        remote_art.query(self._coresong)
 
     @log
     def _remote_art_retrieved(self, klass):
         cache = Cache()
         cache.connect('miss', self._remote_art_unavailable)
         cache.connect('hit', self._cache_hit)
-        cache.query(self._media)
+        cache.query(self._coresong)
 
     @log
     def _remote_art_unavailable(self, klass):
@@ -281,8 +286,12 @@ class Art(GObject.GObject):
 
     @log
     def _add_to_blacklist(self):
-        album = utils.get_album_title(self._media)
-        artist = utils.get_artist_name(self._media)
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            album = self._coresong.props.album
+        except AttributeError:
+            album = self._coresong.props.title
+        artist = self._coresong.props.artist
 
         if artist not in self._blacklist:
             self._blacklist[artist] = []
@@ -292,8 +301,12 @@ class Art(GObject.GObject):
 
     @log
     def _in_blacklist(self):
-        album = utils.get_album_title(self._media)
-        artist = utils.get_artist_name(self._media)
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            album = self._coresong.props.album
+        except AttributeError:
+            album = self._coresong.props.title
+        artist = self._coresong.props.artist
         album_stripped = MediaArt.strip_invalid_entities(album)
 
         if artist in self._blacklist:
@@ -340,12 +353,12 @@ class Cache(GObject.GObject):
                 return
 
     @log
-    def query(self, media):
+    def query(self, coresong):
         """Start the cache query
 
-        :param Grl.Media media: The media object to search art for
+        :param CoreSong coresong: The CoreSong object to search art for
         """
-        thumb_file = lookup_art_file_from_cache(media)
+        thumb_file = lookup_art_file_from_cache(coresong)
         if thumb_file:
             thumb_file.read_async(
                 GLib.PRIORITY_LOW, None, self._open_stream, None)
@@ -415,22 +428,30 @@ class EmbeddedArt(GObject.GObject):
 
         self._album = None
         self._artist = None
-        self._media = None
+        self._coresong = None
         self._path = None
 
     @log
-    def query(self, media):
+    def query(self, coresong):
         """Start the local query
 
-        :param Grl.Media media: The media object to search art for
+        :param CoreSong coresong: The CoreSong object to search art for
         """
-        if media.get_url() is None:
+        try:
+            if coresong.props.url is None:
+                self.emit('unavailable')
+                return
+        except AttributeError:
             self.emit('unavailable')
             return
 
-        self._album = utils.get_album_title(media)
-        self._artist = utils.get_artist_name(media)
-        self._media = media
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            self._album = coresong.props.album
+        except AttributeError:
+            self._album = coresong.props.title
+        self._artist = coresong.props.artist
+        self._coresong = coresong
 
         try:
             discoverer = GstPbutils.Discoverer.new(Gst.SECOND)
@@ -451,7 +472,7 @@ class EmbeddedArt(GObject.GObject):
 
         self._path = path
 
-        success = discoverer.discover_uri_async(self._media.get_url())
+        success = discoverer.discover_uri_async(self._coresong.props.url)
 
         if not success:
             logger.warning("Could not add url to discoverer.")
@@ -510,7 +531,7 @@ class EmbeddedArt(GObject.GObject):
         # Find local art in cover.jpeg files.
         self._media_art.uri_async(
             MediaArt.Type.ALBUM, MediaArt.ProcessFlags.NONE,
-            self._media.get_url(), self._artist, self._album,
+            self._coresong.props.url, self._artist, self._album,
             GLib.PRIORITY_LOW, None, self._uri_async_cb, None)
 
     @log
@@ -554,29 +575,47 @@ class RemoteArt(GObject.GObject):
 
         self._artist = None
         self._album = None
+        self._coresong = None
+        self._grilo = None
 
     @log
-    def query(self, media):
+    def query(self, coresong):
         """Start the remote query
 
-        :param Grl.Media media: The media object to search art for
+        :param CoreSong coresong: The CoreSong object to search art for
         """
-        self._album = utils.get_album_title(media)
-        self._artist = utils.get_artist_name(media)
-        self._media = media
+        # FIXME: coresong can be a CoreAlbum
+        try:
+            self._album = coresong.props.album
+        except AttributeError:
+            self._album = coresong.props.title
+        self._artist = coresong.props.artist
+        self._coresong = coresong
+
+        self.emit('no-remote-sources')
+
+        # FIXME: This is a total hack. It gets CoreModel from the
+        # CoreAlbum or CoreSong about and then retrieves the CoreGrilo
+        # instance.
+        try:
+            self._grilo = self._coresong._coremodel._grilo
+        except AttributeError:
+            self._grilo = self._coresong._grilo
 
-        if not grilo.props.cover_sources:
+        if not self._grilo.props.cover_sources:
             self.emit('no-remote-sources')
-            grilo.connect(
+            self._grilo.connect(
                 'notify::cover-sources', self._on_grilo_cover_sources_changed)
         else:
             # FIXME: It seems this Grilo query does not always return,
             # especially on queries with little info.
-            grilo.get_album_art_for_item(media, self._remote_album_art)
+            self._grilo.get_album_art_for_item(
+                self._coresong, self._remote_album_art)
 
     def _on_grilo_cover_sources_changed(self, klass, data):
-        if grilo.props.cover_sources:
-            grilo.get_album_art_for_item(self._media, self._remote_album_art)
+        if self._grilo.props.cover_sources:
+            self._grilo.get_album_art_for_item(
+                self._coresong, self._remote_album_art)
 
     @log
     def _delete_callback(self, src, result, data):
diff --git a/gnomemusic/application.py b/gnomemusic/application.py
index 4887d8fa..605362e6 100644
--- a/gnomemusic/application.py
+++ b/gnomemusic/application.py
@@ -36,6 +36,8 @@ import logging
 from gi.repository import Gtk, Gio, GLib, Gdk, GObject
 
 from gnomemusic import log
+from gnomemusic.coremodel import CoreModel
+from gnomemusic.coreselection import CoreSelection
 from gnomemusic.inhibitsuspend import InhibitSuspend
 from gnomemusic.mpris import MPRIS
 from gnomemusic.pauseonsuspend import PauseOnSuspend
@@ -53,7 +55,6 @@ class Application(Gtk.Application):
         super().__init__(
             application_id=application_id,
             flags=Gio.ApplicationFlags.FLAGS_NONE)
-
         self.props.resource_base_path = "/org/gnome/Music"
         GLib.set_application_name(_("Music"))
         GLib.set_prgname(application_id)
@@ -62,6 +63,9 @@ class Application(Gtk.Application):
         self._init_style()
         self._window = None
 
+        self._coreselection = CoreSelection()
+        self._coremodel = CoreModel(self._coreselection)
+
         self._settings = Gio.Settings.new('org.gnome.Music')
         self._player = Player(self)
 
@@ -96,6 +100,26 @@ class Application(Gtk.Application):
         """
         return self._settings
 
+    @GObject.Property(
+        type=CoreModel, flags=GObject.ParamFlags.READABLE)
+    def coremodel(self):
+        """Get class providing all listmodels.
+
+        :returns: List model provider class
+        :rtype: CoreModel
+        """
+        return self._coremodel
+
+    @GObject.Property(
+        type=CoreSelection, flags=GObject.ParamFlags.READABLE)
+    def coreselection(self):
+        """Get selection object.
+
+        :returns: Object containing all selection info
+        :rtype: CoreSelection
+        """
+        return self._coreselection
+
     @log
     def _build_app_menu(self):
         action_entries = [
diff --git a/gnomemusic/corealbum.py b/gnomemusic/corealbum.py
new file mode 100644
index 00000000..0301e624
--- /dev/null
+++ b/gnomemusic/corealbum.py
@@ -0,0 +1,99 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import gi
+gi.require_version('Grl', '0.3')
+from gi.repository import Gio, Grl, GObject
+
+from gnomemusic import log
+import gnomemusic.utils as utils
+
+
+class CoreAlbum(GObject.GObject):
+    """Exposes a Grl.Media with relevant data as properties
+    """
+
+    artist = GObject.Property(type=str)
+    composer = GObject.Property(type=str, default=None)
+    duration = GObject.Property(type=int, default=0)
+    media = GObject.Property(type=Grl.Media)
+    title = GObject.Property(type=str)
+    year = GObject.Property(type=str, default="----")
+
+    @log
+    def __init__(self, media, coremodel):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._model = None
+        self._selected = False
+        self.update(media)
+
+    @log
+    def update(self, media):
+        self.props.media = media
+        self.props.artist = utils.get_artist_name(media)
+        self.props.composer = media.get_composer()
+        self.props.title = utils.get_media_title(media)
+        self.props.year = utils.get_media_year(media)
+
+    @GObject.Property(
+        type=Gio.ListModel, default=None, flags=GObject.ParamFlags.READABLE)
+    def model(self):
+        if self._model is None:
+            self._model = self._coremodel.get_album_model(self.props.media)
+            self._model.connect("items-changed", self._on_list_items_changed)
+
+        self._on_list_items_changed(self._model, None, None, None)
+
+        return self._model
+
+    def _on_list_items_changed(self, model, pos, removed, added):
+        with self.freeze_notify():
+            for coredisc in model:
+                coredisc.connect("notify::duration", self._on_duration_changed)
+                coredisc.props.selected = self.props.selected
+
+    def _on_duration_changed(self, coredisc, duration):
+        duration = 0
+
+        for coredisc in self.props.model:
+            duration += coredisc.props.duration
+
+        self.props.duration = duration
+
+    @GObject.Property(type=bool, default=False)
+    def selected(self):
+        return self._selected
+
+    @selected.setter
+    def selected(self, value):
+        self._selected = value
+
+        # The model is loaded on-demand, so the first time the model is
+        # returned it can still be empty. This is problem for returning
+        # a selection. Trigger loading of the model here if a selection
+        # is requested, it will trigger the filled model update as
+        # well.
+        self.props.model
diff --git a/gnomemusic/coreartist.py b/gnomemusic/coreartist.py
new file mode 100644
index 00000000..fcb482bc
--- /dev/null
+++ b/gnomemusic/coreartist.py
@@ -0,0 +1,84 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import gi
+gi.require_version('Grl', '0.3')
+from gi.repository import Gio, Grl, GObject
+
+from gnomemusic import log
+import gnomemusic.utils as utils
+
+
+class CoreArtist(GObject.GObject):
+    """Exposes a Grl.Media with relevant data as properties
+    """
+
+    artist = GObject.Property(type=str)
+    media = GObject.Property(type=Grl.Media)
+
+    @log
+    def __init__(self, media, coremodel):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._model = None
+        self._selected = False
+
+        self.update(media)
+
+    @log
+    def update(self, media):
+        self.props.media = media
+        self.props.artist = utils.get_artist_name(media)
+
+    @GObject.Property(type=Gio.ListModel, default=None)
+    def model(self):
+        if self._model is None:
+            self._model = self._coremodel.get_artist_album_model(
+                self.props.media)
+            self._model.connect("items-changed", self._on_items_changed)
+
+        self._on_items_changed(self._model, None, None, None)
+
+        return self._model
+
+    def _on_items_changed(self, model, pos, removed, added):
+        with self.freeze_notify():
+            for corealbum in self._model:
+                corealbum.props.selected = self.props.selected
+
+    @GObject.Property(type=bool, default=False)
+    def selected(self):
+        return self._selected
+
+    @selected.setter
+    def selected(self, value):
+        self._selected = value
+
+        # The model is loaded on-demand, so the first time the model is
+        # returned it can still be empty. This is problem for returning
+        # a selection. Trigger loading of the model here if a selection
+        # is requested, it will trigger the filled model update as
+        # well.
+        self.props.model
diff --git a/gnomemusic/coredisc.py b/gnomemusic/coredisc.py
new file mode 100644
index 00000000..c3139174
--- /dev/null
+++ b/gnomemusic/coredisc.py
@@ -0,0 +1,141 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# 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 gi.repository import Dazzle, GObject, Gio, Gfm, Grl
+from gi._gi import pygobject_new_full
+
+
+class CoreDisc(GObject.GObject):
+
+    disc_nr = GObject.Property(type=int, default=0)
+    duration = GObject.Property(type=int, default=None)
+    media = GObject.Property(type=Grl.Media, default=None)
+
+    def __init__(self, media, nr, coremodel):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._filter_model = None
+        self._model = None
+        self._old_album_ids = []
+        self._selected = False
+        self._sort_model = None
+
+        self.update(media)
+        self.props.disc_nr = nr
+
+    def update(self, media):
+        self.props.media = media
+
+    @GObject.Property(type=Gio.ListModel, default=None)
+    def model(self):
+        if self._model is None:
+            self._filter_model = Dazzle.ListModelFilter.new(
+                self._coremodel.props.songs)
+            self._filter_model.set_filter_func(lambda a: False)
+            self._sort_model = Gfm.SortListModel.new(self._filter_model)
+            self._sort_model.set_sort_func(
+                self._wrap_sort_func(self._disc_sort))
+
+            self._model = self._sort_model
+
+            self._coremodel.props.songs.connect(
+                "items-changed", self._on_core_changed)
+            self._model.connect("items-changed", self._on_disc_changed)
+
+            self._get_album_disc(
+                self.props.media, self.props.disc_nr, self._filter_model)
+
+        self._on_disc_changed(self._model, None, None, None)
+
+        return self._model
+
+    def _on_core_changed(self, model, position, removed, added):
+        self._get_album_disc(
+            self.props.media, self.props.disc_nr, self._filter_model)
+
+    def _on_disc_changed(self, model, position, removed, added):
+        with self.freeze_notify():
+            for coresong in model:
+                coresong.props.selected = self._selected
+
+    def _update_duration(self):
+        duration = 0
+
+        for coresong in self.props.model:
+            duration += coresong.props.duration
+
+        self.props.duration = duration
+
+    def _disc_sort(self, song_a, song_b):
+        return song_a.props.track_number - song_b.props.track_number
+
+    def _wrap_sort_func(self, func):
+
+        def wrap(a, b, *user_data):
+            a = pygobject_new_full(a, False)
+            b = pygobject_new_full(b, False)
+            return func(a, b, *user_data)
+
+        return wrap
+
+    def _get_album_disc(self, media, discnr, model):
+        album_ids = []
+        model_filter = model
+
+        def _filter_func(core_song):
+            return core_song.props.grlid in album_ids
+
+        def _reverse_sort(song_a, song_b, data=None):
+            return song_a.props.track_number - song_b.props.track_number
+
+        def _callback(source, dunno, media, something, something2):
+            if media is None:
+                if sorted(album_ids) == sorted(self._old_album_ids):
+                    return
+                model_filter.set_filter_func(_filter_func)
+                self._old_album_ids = album_ids
+                self._update_duration()
+                return
+
+            album_ids.append(media.get_source() + media.get_id())
+
+        self._coremodel._grilo.populate_album_disc_songs(
+            media, discnr, _callback)
+
+    @GObject.Property(
+        type=bool, default=False, flags=GObject.BindingFlags.SYNC_CREATE)
+    def selected(self):
+        return self._selected
+
+    @selected.setter
+    def selected(self, value):
+        self._selected = value
+
+        # The model is loaded on-demand, so the first time the model is
+        # returned it can still be empty. This is problem for returning
+        # a selection. Trigger loading of the model here if a selection
+        # is requested, it will trigger the filled model update as
+        # well.
+        self.props.model
diff --git a/gnomemusic/coregrilo.py b/gnomemusic/coregrilo.py
new file mode 100644
index 00000000..fcc0d9e0
--- /dev/null
+++ b/gnomemusic/coregrilo.py
@@ -0,0 +1,207 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import gi
+gi.require_version('Grl', '0.3')
+from gi.repository import Grl, GLib, GObject
+
+# from gnomemusic.grilowrappers.grldleynawrapper import GrlDLeynaWrapper
+from gnomemusic.grilowrappers.grlsearchwrapper import GrlSearchWrapper
+from gnomemusic.grilowrappers.grltrackerwrapper import GrlTrackerWrapper
+
+
+class CoreGrilo(GObject.GObject):
+
+    _blacklist = [
+        'grl-bookmarks',
+        'grl-filesystem',
+        'grl-itunes-podcast',
+        'grl-metadata-store',
+        'grl-podcasts',
+        'grl-spotify-cover'
+    ]
+
+    _theaudiodb_api_key = "195003"
+
+    cover_sources = GObject.Property(type=bool, default=False)
+    tracker_available = GObject.Property(type=bool, default=False)
+
+    def __repr__(self):
+        return "<CoreGrilo>"
+
+    def __init__(self, coremodel, coreselection):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._coreselection = coreselection
+        self._search_wrappers = {}
+        self._thumbnail_sources = []
+        self._thumbnail_sources_timeout = None
+        self._wrappers = {}
+
+        Grl.init(None)
+
+        self._registry = Grl.Registry.get_default()
+        config = Grl.Config.new("grl-lua-factory", "grl-theaudiodb-cover")
+        config.set_api_key(self._theaudiodb_api_key)
+        self._registry.add_config(config)
+
+        self._registry.connect('source-added', self._on_source_added)
+        self._registry.connect('source-removed', self._on_source_removed)
+
+        self._registry.load_all_plugins(True)
+
+    def _on_source_added(self, registry, source):
+
+        def _trigger_art_update():
+            self._thumbnail_sources_timeout = None
+            if len(self._thumbnail_sources) > 0:
+                self.props.cover_sources = True
+
+            return GLib.SOURCE_REMOVE
+
+        if ("net:plaintext" in source.get_tags()
+                or source.props.source_id in self._blacklist):
+            try:
+                registry.unregister_source(source)
+            except GLib.GError:
+                print("Failed to unregister {}".format(
+                    source.props.source_id))
+            return
+
+        if Grl.METADATA_KEY_THUMBNAIL in source.supported_keys():
+            self._thumbnail_sources.append(source)
+            if not self._thumbnail_sources_timeout:
+                # Aggregate sources being added, for example when the
+                # network comes online.
+                self._thumbnail_sources_timeout = GLib.timeout_add_seconds(
+                    5, _trigger_art_update)
+
+        new_wrapper = None
+
+        if (source.props.source_id == "grl-tracker-source"
+                and source.props.source_id not in self._wrappers.keys()):
+            new_wrapper = GrlTrackerWrapper(
+                source, self._coremodel, self._coreselection, self)
+            self._wrappers[source.props.source_id] = new_wrapper
+            self.props.tracker_available = True
+        # elif source.props.source_id[:10] == "grl-dleyna":
+        #     new_wrapper = GrlDLeynaWrapper(
+        #         source, self._coremodel, self._coreselection, self)
+        #     self._wrappers.append(new_wrapper)
+            print("wrapper", new_wrapper)
+        elif (source.props.source_id not in self._search_wrappers.keys()
+                and source.get_supported_media() & Grl.MediaType.AUDIO
+                and source.supported_operations() & Grl.SupportedOps.SEARCH):
+            self._search_wrappers[source.props.source_id] = GrlSearchWrapper(
+                source, self._coremodel, self._coreselection, self)
+            print("search source", source)
+
+    def _on_source_removed(self, registry, source):
+        # FIXME: Handle removing sources.
+        print("removed,", source.props.source_id)
+
+        # FIXME: Only removes search sources atm.
+        self._search_wrappers.pop(source.props.source_id, None)
+
+    def get_artist_albums(self, artist, filter_model):
+        for wrapper in self._wrappers.values():
+            wrapper.get_artist_albums(artist, filter_model)
+
+    def get_album_discs(self, media, disc_model):
+        for wrapper in self._wrappers.values():
+            wrapper.get_album_discs(media, disc_model)
+
+    def populate_album_disc_songs(self, media, discnr, callback):
+        for wrapper in self._wrappers.values():
+            wrapper.populate_album_disc_songs(media, discnr, callback)
+
+    def _store_metadata(self, source, media, key):
+        """Convenience function to store metadata
+
+        Wrap the metadata store call in a idle_add compatible form.
+        :param source: A Grilo source object
+        :param media: A Grilo media item
+        :param key: A Grilo metadata key
+        """
+        # FIXME: Doing this async crashes.
+        try:
+            source.store_metadata_sync(
+                media, [key], Grl.WriteFlags.NORMAL)
+        except GLib.Error as error:
+            # FIXME: Do not print.
+            print("Error {}: {}".format(error.domain, error.message))
+
+        return GLib.SOURCE_REMOVE
+
+    def writeback(self, media, key):
+        for wrapper in self._wrappers.values():
+            if media.get_source() == wrapper.source.props.source_id:
+                GLib.idle_add(
+                    self._store_metadata, wrapper.props.source, media, key)
+                break
+
+    def search(self, text):
+        for wrapper in self._wrappers.values():
+            wrapper.search(text)
+        for wrapper in self._search_wrappers.values():
+            wrapper.search(text)
+
+    def get_album_art_for_item(self, coresong, callback):
+        # Tracker not (yet) loaded.
+        if "grl-tracker-source" not in self._wrappers:
+            self._wrappers["grl-tracker-source"].get_album_art_for_item(
+                coresong, callback)
+
+    def stage_playlist_deletion(self, playlist):
+        """Prepares playlist deletion.
+
+        :param Playlist playlist: playlist
+        """
+        for wrapper in self._wrappers:
+            if wrapper.source.props.source_id == "grl-tracker-source":
+                wrapper.stage_playlist_deletion(playlist)
+                break
+
+    def finish_playlist_deletion(self, playlist, deleted):
+        """Finishes playlist deletion.
+
+        :param Playlist playlist: playlist
+        :param bool deleted: indicates if the playlist has been deleted
+        """
+        for wrapper in self._wrappers:
+            if wrapper.source.props.source_id == "grl-tracker-source":
+                wrapper.finish_playlist_deletion(playlist, deleted)
+                break
+
+    def create_playlist(self, playlist_title, callback):
+        """Creates a new user playlist.
+
+        :param str playlist_title: playlist title
+        :param callback: function to perform once, the playlist is created
+        """
+        for wrapper in self._wrappers:
+            if wrapper.source.props.source_id == "grl-tracker-source":
+                wrapper.create_playlist(playlist_title, callback)
+                break
diff --git a/gnomemusic/coremodel.py b/gnomemusic/coremodel.py
new file mode 100644
index 00000000..23a83401
--- /dev/null
+++ b/gnomemusic/coremodel.py
@@ -0,0 +1,456 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import math
+
+import gi
+gi.require_versions({'Dazzle': '1.0', 'Gfm': '0.1'})
+from gi.repository import Dazzle, GObject, Gio, Gfm, Gtk
+from gi._gi import pygobject_new_full
+
+from gnomemusic.coreartist import CoreArtist
+from gnomemusic.coregrilo import CoreGrilo
+from gnomemusic.coresong import CoreSong
+from gnomemusic.grilowrappers.grltrackerplaylists import Playlist
+from gnomemusic.player import PlayerPlaylist
+from gnomemusic.songliststore import SongListStore
+from gnomemusic.widgets.songwidget import SongWidget
+
+
+class CoreModel(GObject.GObject):
+    """Provides all the list models used in Music
+
+    Music is using a hierarchy of data objects with list models to
+    contain the information about the users available music. This
+    hierarchy is filled mainly through Grilo, with the exception of
+    playlists which are a Tracker only feature.
+
+    There are three main models: one for artist info, one for albums
+    and one for songs. The data objects within these are CoreArtist,
+    CoreAlbum and CoreSong respectively.
+
+    The data objects contain filtered lists of the three main models.
+    This makes the hierarchy as follows.
+
+    CoreArtist -> CoreAlbum -> CoreDisc -> CoreSong
+
+    Playlists are a Tracker only feature and do not use the three
+    main models directly.
+
+    GrlTrackerPlaylists -> Playlist -> CoreSong
+
+    The Player playlist is a copy of the relevant playlist, built by
+    using the components described above as needed.
+    """
+
+    __gsignals__ = {
+        "activate-playlist": (
+            GObject.SignalFlags.RUN_FIRST, None, (Playlist,)),
+        "artists-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "playlist-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "playlists-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
+    }
+
+    songs_available = GObject.Property(type=bool, default=False)
+
+    def __init__(self, coreselection):
+        super().__init__()
+
+        self._flatten_model = None
+        self._search_signal_id = None
+        self._song_signal_id = None
+
+        self._model = Gio.ListStore.new(CoreSong)
+        self._songliststore = SongListStore(self._model)
+
+        self._coreselection = coreselection
+        self._album_model = Gio.ListStore()
+        self._album_model_sort = Gfm.SortListModel.new(self._album_model)
+        self._album_model_sort.set_sort_func(
+            self._wrap_list_store_sort_func(self._albums_sort))
+
+        self._artist_model = Gio.ListStore.new(CoreArtist)
+        self._artist_model_sort = Gfm.SortListModel.new(self._artist_model)
+        self._artist_model_sort.set_sort_func(
+            self._wrap_list_store_sort_func(self._artist_sort))
+
+        self._playlist_model = Gio.ListStore.new(CoreSong)
+        self._playlist_model_sort = Gfm.SortListModel.new(self._playlist_model)
+
+        self._song_search_proxy = Gio.ListStore.new(Gfm.FilterListModel)
+        self._song_search_flatten = Gfm.FlattenListModel.new(CoreSong)
+        self._song_search_flatten.set_model(self._song_search_proxy)
+
+        self._album_search_model = Dazzle.ListModelFilter.new(
+            self._album_model)
+        self._album_search_model.set_filter_func(lambda a: False)
+
+        self._artist_search_model = Dazzle.ListModelFilter.new(
+            self._artist_model)
+        self._artist_search_model.set_filter_func(lambda a: False)
+
+        self._playlists_model = Gio.ListStore.new(Playlist)
+        self._playlists_model_filter = Dazzle.ListModelFilter.new(
+            self._playlists_model)
+        self._playlists_model_sort = Gfm.SortListModel.new(
+            self._playlists_model_filter)
+        self._playlists_model_sort.set_sort_func(
+            self._wrap_list_store_sort_func(self._playlists_sort))
+
+        self._grilo = CoreGrilo(self, self._coreselection)
+
+        self._model.connect("items-changed", self._on_songs_items_changed)
+
+    def _on_songs_items_changed(self, model, position, removed, added):
+        available = self.props.songs_available
+        now_available = model.get_n_items() > 0
+
+        if available == now_available:
+            return
+
+        if model.get_n_items() > 0:
+            self.props.songs_available = True
+        else:
+            self.props.songs_available = False
+
+    def _filter_selected(self, coresong):
+        return coresong.props.selected
+
+    def _albums_sort(self, album_a, album_b):
+        return album_b.props.title.casefold() < album_a.props.title.casefold()
+
+    def _artist_sort(self, artist_a, artist_b):
+        name_a = artist_a.props.artist.casefold()
+        name_b = artist_b.props.artist.casefold()
+        return name_a > name_b
+
+    def _playlists_sort(self, playlist_a, playlist_b):
+        if playlist_a.props.is_smart:
+            if not playlist_b.props.is_smart:
+                return -1
+            title_a = playlist_a.props.title.casefold()
+            title_b = playlist_b.props.title.casefold()
+            return title_a > title_b
+
+        if playlist_b.props.is_smart:
+            return 1
+
+        # cannot use GLib.DateTime.compare
+        # https://gitlab.gnome.org/GNOME/pygobject/issues/334
+        # newest first
+        date_diff = playlist_b.props.creation_date.difference(
+            playlist_a.props.creation_date)
+        return math.copysign(1, date_diff)
+
+    def _wrap_list_store_sort_func(self, func):
+
+        def wrap(a, b, *user_data):
+            a = pygobject_new_full(a, False)
+            b = pygobject_new_full(b, False)
+            return func(a, b, *user_data)
+
+        return wrap
+
+    def get_album_model(self, media):
+        disc_model = Gio.ListStore()
+        disc_model_sort = Gfm.SortListModel.new(disc_model)
+
+        def _disc_order_sort(disc_a, disc_b):
+            return disc_a.props.disc_nr - disc_b.props.disc_nr
+
+        disc_model_sort.set_sort_func(
+            self._wrap_list_store_sort_func(_disc_order_sort))
+
+        self._grilo.get_album_discs(media, disc_model)
+
+        return disc_model_sort
+
+    def get_artist_album_model(self, media):
+        albums_model_filter = Dazzle.ListModelFilter.new(self._album_model)
+        albums_model_filter.set_filter_func(lambda a: False)
+
+        albums_model_sort = Gfm.SortListModel.new(albums_model_filter)
+
+        self._grilo.get_artist_albums(media, albums_model_filter)
+
+        def _album_sort(album_a, album_b):
+            return album_a.props.year > album_b.props.year
+
+        albums_model_sort.set_sort_func(
+            self._wrap_list_store_sort_func(_album_sort))
+
+        return albums_model_sort
+
+    def set_playlist_model(self, playlist_type, model):
+
+        def _on_items_changed(model, position, removed, added):
+            if removed > 0:
+                for i in list(range(removed)):
+                    self._playlist_model.remove(position)
+
+            if added > 0:
+                for i in list(range(added)):
+                    coresong = model[position + i]
+                    song = CoreSong(
+                        coresong.props.media, self._coreselection,
+                        self._grilo)
+
+                    self._playlist_model.insert(position + i, song)
+
+                    song.bind_property(
+                        "state", coresong, "state",
+                        GObject.BindingFlags.SYNC_CREATE)
+                    song.bind_property(
+                        "validation", coresong, "validation",
+                        GObject.BindingFlags.SYNC_CREATE)
+
+        with model.freeze_notify():
+
+            if playlist_type == PlayerPlaylist.Type.ALBUM:
+
+                self._playlist_model.remove_all()
+                proxy_model = Gio.ListStore.new(Gio.ListModel)
+
+                for disc in model:
+                    proxy_model.append(disc.props.model)
+
+                self._flatten_model = Gfm.FlattenListModel.new(
+                    CoreSong, proxy_model)
+                self._flatten_model.connect("items-changed", _on_items_changed)
+
+                for model_song in self._flatten_model:
+                    song = CoreSong(
+                        model_song.props.media, self._coreselection,
+                        self._grilo)
+
+                    self._playlist_model.append(song)
+                    song.bind_property(
+                        "state", model_song, "state",
+                        GObject.BindingFlags.SYNC_CREATE)
+                    song.bind_property(
+                        "validation", model_song, "validation",
+                        GObject.BindingFlags.SYNC_CREATE)
+
+                self.emit("playlist-loaded")
+            elif playlist_type == PlayerPlaylist.Type.ARTIST:
+                self._playlist_model.remove_all()
+                proxy_model = Gio.ListStore.new(Gio.ListModel)
+
+                for artist_album in model:
+                    for disc in artist_album.model:
+                        proxy_model.append(disc.props.model)
+
+                self._flatten_model = Gfm.FlattenListModel.new(
+                    CoreSong, proxy_model)
+                self._flatten_model.connect("items-changed", _on_items_changed)
+
+                for model_song in self._flatten_model:
+                    song = CoreSong(
+                        model_song.props.media, self._coreselection,
+                        self._grilo)
+
+                    self._playlist_model.append(song)
+                    song.bind_property(
+                        "state", model_song, "state",
+                        GObject.BindingFlags.SYNC_CREATE)
+                    song.bind_property(
+                        "validation", model_song, "validation",
+                        GObject.BindingFlags.SYNC_CREATE)
+
+                self.emit("playlist-loaded")
+            elif playlist_type == PlayerPlaylist.Type.SONGS:
+                if self._song_signal_id:
+                    self._songliststore.props.model.disconnect(
+                        self._song_signal_id)
+
+                self._playlist_model.remove_all()
+
+                for song in self._songliststore.props.model:
+                    self._playlist_model.append(song)
+
+                    if song.props.state == SongWidget.State.PLAYING:
+                        song.props.state = SongWidget.State.PLAYED
+
+                self._song_signal_id = self._songliststore.props.model.connect(
+                    "items-changed", _on_items_changed)
+
+                self.emit("playlist-loaded")
+            elif playlist_type == PlayerPlaylist.Type.SEARCH_RESULT:
+                if self._search_signal_id:
+                    self._song_search_flatten.disconnect(
+                        self._search_signal_id)
+
+                self._playlist_model.remove_all()
+
+                for song in self._song_search_flatten:
+                    self._playlist_model.append(song)
+
+                self._search_signal_id = self._song_search_flatten.connect(
+                    "items-changed", _on_items_changed)
+
+                self.emit("playlist-loaded")
+            elif playlist_type == PlayerPlaylist.Type.PLAYLIST:
+                # if self._search_signal_id:
+                #     self._song_search_model.disconnect(self._search_signal_id)
+
+                self._playlist_model.remove_all()
+
+                for model_song in model:
+                    song = CoreSong(
+                        model_song.props.media, self._coreselection,
+                        self._grilo)
+
+                    self._playlist_model.append(song)
+
+                    song.bind_property(
+                        "state", model_song, "state",
+                        GObject.BindingFlags.SYNC_CREATE)
+                    song.bind_property(
+                        "validation", model_song, "validation",
+                        GObject.BindingFlags.SYNC_CREATE)
+
+                # self._search_signal_id = self._song_search_model.connect(
+                #     "items-changed", _on_items_changed)
+
+                self.emit("playlist-loaded")
+
+    def stage_playlist_deletion(self, playlist):
+        """Prepares playlist deletion.
+
+        :param Playlist playlist: playlist
+        """
+        self._grilo.stage_playlist_deletion(playlist)
+
+    def finish_playlist_deletion(self, playlist, deleted):
+        """Finishes playlist deletion.
+
+        :param Playlist playlist: playlist
+        :param bool deleted: indicates if the playlist has been deleted
+        """
+        self._grilo.finish_playlist_deletion(playlist, deleted)
+
+    def create_playlist(self, playlist_title, callback):
+        """Creates a new user playlist.
+
+        :param str playlist_title: playlist title
+        :param callback: function to perform once, the playlist is created
+        """
+        self._grilo.create_playlist(playlist_title, callback)
+
+    def activate_playlist(self, playlist):
+        """Activates a playlist.
+
+        Selects the playlist and start playing.
+
+        :param Playlist playlist: playlist to activate
+        """
+        # FIXME: just a proxy
+        self.emit("activate-playlist", playlist)
+
+    def search(self, text):
+        self._grilo.search(text)
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def songs(self):
+        return self._model
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def albums(self):
+        return self._album_model
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def artists(self):
+        return self._artist_model
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def playlist(self):
+        return self._playlist_model
+
+    @GObject.Property(
+        type=Gfm.SortListModel, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def albums_sort(self):
+        return self._album_model_sort
+
+    @GObject.Property(
+        type=Gfm.SortListModel, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def artists_sort(self):
+        return self._artist_model_sort
+
+    @GObject.Property(
+        type=Gfm.SortListModel, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def playlist_sort(self):
+        return self._playlist_model_sort
+
+    @GObject.Property(
+        type=Dazzle.ListModelFilter, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def songs_search(self):
+        return self._song_search_flatten
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def songs_search_proxy(self):
+        return self._song_search_proxy
+
+    @GObject.Property(
+        type=Dazzle.ListModelFilter, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def albums_search(self):
+        return self._album_search_model
+
+    @GObject.Property(
+        type=Dazzle.ListModelFilter, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def artists_search(self):
+        return self._artist_search_model
+
+    @GObject.Property(
+        type=Gtk.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def songs_gtkliststore(self):
+        return self._songliststore
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def playlists(self):
+        return self._playlists_model
+
+    @GObject.Property(
+        type=Gfm.SortListModel, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def playlists_sort(self):
+        return self._playlists_model_sort
+
+    @GObject.Property(
+        type=Gfm.SortListModel, default=None,
+        flags=GObject.ParamFlags.READABLE)
+    def playlists_filter(self):
+        return self._playlists_model_filter
diff --git a/gnomemusic/coreselection.py b/gnomemusic/coreselection.py
new file mode 100644
index 00000000..848da8bc
--- /dev/null
+++ b/gnomemusic/coreselection.py
@@ -0,0 +1,50 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# 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 gi.repository import GObject
+
+
+class CoreSelection(GObject.GObject):
+
+    selected_items_count = GObject.Property(type=int, default=0)
+
+    def __init__(self):
+        super().__init__()
+
+        self._selected_items = []
+
+    def update_selection(self, coresong, value):
+        if coresong.props.selected:
+            self.props.selected_items.append(coresong)
+        else:
+            try:
+                self.props.selected_items.remove(coresong)
+            except ValueError:
+                pass
+
+        self.props.selected_items_count = len(self.props.selected_items)
+
+    @GObject.Property
+    def selected_items(self):
+        return self._selected_items
diff --git a/gnomemusic/coresong.py b/gnomemusic/coresong.py
new file mode 100644
index 00000000..a79f81ed
--- /dev/null
+++ b/gnomemusic/coresong.py
@@ -0,0 +1,120 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# 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 enum import IntEnum
+
+import gi
+gi.require_version('Grl', '0.3')
+from gi.repository import Grl, GLib, GObject
+
+import gnomemusic.utils as utils
+
+
+class CoreSong(GObject.GObject):
+    """Exposes a Grl.Media with relevant data as properties
+    """
+
+    album = GObject.Property(type=str)
+    album_disc_number = GObject.Property(type=int)
+    artist = GObject.Property(type=str)
+    duration = GObject.Property(type=int)
+    media = GObject.Property(type=Grl.Media)
+    grlid = GObject.Property(type=str, default=None)
+    play_count = GObject.Property(type=int)
+    state = GObject.Property()  # FIXME: How to set an IntEnum type?
+    title = GObject.Property(type=str)
+    track_number = GObject.Property(type=int)
+    url = GObject.Property(type=str)
+    validation = GObject.Property()  # FIXME: How to set an IntEnum type?
+
+    class Validation(IntEnum):
+        """Enum for song validation"""
+        PENDING = 0
+        IN_PROGRESS = 1
+        FAILED = 2
+        SUCCEEDED = 3
+
+    def __init__(self, media, coreselection, grilo):
+        super().__init__()
+
+        self._grilo = grilo
+        self._coreselection = coreselection
+        self._favorite = False
+        self._selected = False
+
+        self.props.grlid = media.get_source() + media.get_id()
+        self.props.validation = CoreSong.Validation.PENDING
+        self.update(media)
+
+    def __eq__(self, other):
+        return (isinstance(other, CoreSong)
+                and other.props.media.get_id() == self.props.media.get_id())
+
+    @GObject.Property(type=bool, default=False)
+    def favorite(self):
+        return self._favorite
+
+    @favorite.setter
+    def favorite(self, favorite):
+        self._favorite = favorite
+
+        # FIXME: Circular trigger, can probably be solved more neatly.
+        old_fav = self.props.media.get_favourite()
+        if old_fav == self._favorite:
+            return
+
+        self.props.media.set_favourite(self._favorite)
+        self._grilo.writeback(self.props.media, Grl.METADATA_KEY_FAVOURITE)
+
+    @GObject.Property(type=bool, default=False)
+    def selected(self):
+        return self._selected
+
+    @selected.setter
+    def selected(self, value):
+        if self._selected == value:
+            return
+
+        self._selected = value
+        self._coreselection.update_selection(self, self._selected)
+
+    def update(self, media):
+        self.props.media = media
+        self.props.album = utils.get_album_title(media)
+        self.props.album_disc_number = media.get_album_disc_number()
+        self.props.artist = utils.get_artist_name(media)
+        self.props.duration = media.get_duration()
+        self.props.favorite = media.get_favourite()
+        self.props.play_count = media.get_play_count()
+        self.props.title = utils.get_media_title(media)
+        self.props.track_number = media.get_track_number()
+        self.props.url = media.get_url()
+
+    def bump_play_count(self):
+        self.props.media.set_play_count(self.props.play_count + 1)
+        self._grilo.writeback(self.props.media, Grl.METADATA_KEY_PLAY_COUNT)
+
+    def set_last_played(self):
+        self.props.media.set_last_played(GLib.DateTime.new_now_utc())
+        self._grilo.writeback(self.props.media, Grl.METADATA_KEY_LAST_PLAYED)
diff --git a/gnomemusic/grilowrappers/__init__.py b/gnomemusic/grilowrappers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gnomemusic/grilowrappers/grldleynawrapper.py b/gnomemusic/grilowrappers/grldleynawrapper.py
new file mode 100644
index 00000000..4bc62596
--- /dev/null
+++ b/gnomemusic/grilowrappers/grldleynawrapper.py
@@ -0,0 +1,121 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import gi
+gi.require_version('Grl', '0.3')
+from gi.repository import Grl, GObject
+
+from gnomemusic.coreartist import CoreArtist
+
+
+class GrlDLeynaWrapper(GObject.GObject):
+
+    METADATA_KEYS = [
+        Grl.METADATA_KEY_ALBUM,
+        Grl.METADATA_KEY_ALBUM_ARTIST,
+        Grl.METADATA_KEY_ALBUM_DISC_NUMBER,
+        Grl.METADATA_KEY_ARTIST,
+        Grl.METADATA_KEY_CREATION_DATE,
+        Grl.METADATA_KEY_COMPOSER,
+        Grl.METADATA_KEY_DURATION,
+        Grl.METADATA_KEY_FAVOURITE,
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_LYRICS,
+        Grl.METADATA_KEY_PLAY_COUNT,
+        Grl.METADATA_KEY_THUMBNAIL,
+        Grl.METADATA_KEY_TITLE,
+        Grl.METADATA_KEY_TRACK_NUMBER,
+        Grl.METADATA_KEY_URL
+    ]
+
+    def __repr__(self):
+        return "<GrlDLeynaWrapper>"
+
+    def __init__(self, source, coremodel, core_selection, grilo):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._core_selection = core_selection
+        self._grilo = grilo
+        self._source = source
+        self._model = self._coremodel.props.songs
+        self._albums_model = self._coremodel.props.albums
+        self._album_ids = {}
+        self._artists_model = self._coremodel.props.artists
+
+        Grl.init(None)
+
+        self._fast_options = Grl.OperationOptions()
+        self._fast_options.set_resolution_flags(
+            Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY)
+
+        self._full_options = Grl.OperationOptions()
+        self._full_options.set_resolution_flags(
+            Grl.ResolutionFlags.FULL | Grl.ResolutionFlags.IDLE_RELAY)
+
+        # self._initial_fill(self._source)
+        # self._initial_albums_fill(self._source)
+        self._initial_artists_fill(self._source)
+
+        # self._source.connect("content-changed", self._on_content_changed)
+
+    @GObject.Property(
+        type=Grl.Source, default=None, flags=GObject.ParamFlags.READABLE)
+    def source(self):
+        return self._source
+
+    def _initial_artists_fill(self, source):
+        query = """
+        upnp:class derivedfrom 'object.container.person.musicArtist'
+        """.replace('\n', ' ').strip()
+
+        options = self._fast_options.copy()
+
+        source.query(
+            query, self.METADATA_KEYS, options, self._add_to_artists_model)
+
+    def _add_to_artists_model(self, source, op_id, media, user_data, error):
+        if error:
+            print("ERROR", error)
+            return
+
+        if not media:
+            print("NO MEDIA", source, op_id, media, error)
+            return
+
+        artist = CoreArtist(media, self._coremodel, self._grilo)
+        artist.props.artist = media.get_title() + " (upnp)"
+        self._artists_model.append(artist)
+        print(
+            "ADDING DLNA ARTIST", media.get_title(), media.get_artist(),
+            media.get_id())
+
+    def get_artist_albums(self, artist, filter_model):
+        pass
+
+    def populate_album_disc_songs(self, media, discnr, callback):
+        pass
+
+    def search(self, text):
+        pass
diff --git a/gnomemusic/grilowrappers/grlsearchwrapper.py b/gnomemusic/grilowrappers/grlsearchwrapper.py
new file mode 100644
index 00000000..98ca0471
--- /dev/null
+++ b/gnomemusic/grilowrappers/grlsearchwrapper.py
@@ -0,0 +1,95 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import gi
+gi.require_versions({"Grl": "0.3"})
+from gi.repository import Gfm, Gio, Grl, GObject
+
+from gnomemusic.coresong import CoreSong
+
+
+class GrlSearchWrapper(GObject.GObject):
+
+    METADATA_KEYS = [
+        Grl.METADATA_KEY_ALBUM,
+        Grl.METADATA_KEY_ALBUM_ARTIST,
+        Grl.METADATA_KEY_ALBUM_DISC_NUMBER,
+        Grl.METADATA_KEY_ARTIST,
+        Grl.METADATA_KEY_CREATION_DATE,
+        Grl.METADATA_KEY_COMPOSER,
+        Grl.METADATA_KEY_DURATION,
+        Grl.METADATA_KEY_FAVOURITE,
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_PLAY_COUNT,
+        Grl.METADATA_KEY_THUMBNAIL,
+        Grl.METADATA_KEY_TITLE,
+        Grl.METADATA_KEY_TRACK_NUMBER,
+        Grl.METADATA_KEY_URL
+    ]
+
+    def __repr__(self):
+        return "<GrlSearchWrapper>"
+
+    def __init__(self, source, coremodel, coreselection, grilo):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._coreselection = coreselection
+        self._grilo = grilo
+        self._source = source
+
+        self._song_search_proxy = self._coremodel.props.songs_search_proxy
+
+        self._song_search_store = Gio.ListStore.new(CoreSong)
+        # FIXME: Workaround for adding the right list type to the proxy
+        # list model.
+        self._song_search_model = Gfm.FilterListModel.new(
+            self._song_search_store)
+        self._song_search_model.set_filter_func(lambda a: True)
+        self._song_search_proxy.append(self._song_search_model)
+
+        self._fast_options = Grl.OperationOptions()
+        self._fast_options.set_count(25)
+        self._fast_options.set_resolution_flags(
+            Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY)
+
+    def search(self, text):
+        with self._song_search_store.freeze_notify():
+            self._song_search_store.remove_all()
+
+        def _search_result_cb(source, op_id, media, remaining, error):
+            if error:
+                print("error")
+                return
+            if media is None:
+                return
+
+            coresong = CoreSong(media, self._coreselection, self._grilo)
+            coresong.props.title = (
+                coresong.props.title + " (" + source.props.source_id + ")")
+
+            self._song_search_store.append(coresong)
+
+        self._source.search(
+            text, self.METADATA_KEYS, self._fast_options, _search_result_cb)
diff --git a/gnomemusic/grilowrappers/grltrackerplaylists.py b/gnomemusic/grilowrappers/grltrackerplaylists.py
new file mode 100644
index 00000000..b7248fed
--- /dev/null
+++ b/gnomemusic/grilowrappers/grltrackerplaylists.py
@@ -0,0 +1,851 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import time
+
+from gettext import gettext as _
+
+import gi
+gi.require_versions({"Grl": "0.3"})
+from gi.repository import Gio, Grl, GLib, GObject
+
+from gnomemusic.coresong import CoreSong
+from gnomemusic.trackerwrapper import TrackerWrapper
+import gnomemusic.utils as utils
+
+
+class GrlTrackerPlaylists(GObject.GObject):
+
+    METADATA_KEYS = [
+        Grl.METADATA_KEY_ALBUM,
+        Grl.METADATA_KEY_ALBUM_ARTIST,
+        Grl.METADATA_KEY_ALBUM_DISC_NUMBER,
+        Grl.METADATA_KEY_ARTIST,
+        Grl.METADATA_KEY_CHILDCOUNT,
+        Grl.METADATA_KEY_CREATION_DATE,
+        Grl.METADATA_KEY_COMPOSER,
+        Grl.METADATA_KEY_DURATION,
+        Grl.METADATA_KEY_FAVOURITE,
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_PLAY_COUNT,
+        Grl.METADATA_KEY_THUMBNAIL,
+        Grl.METADATA_KEY_TITLE,
+        Grl.METADATA_KEY_TRACK_NUMBER,
+        Grl.METADATA_KEY_URL
+    ]
+
+    def __repr__(self):
+        return "<GrlTrackerPlaylists>"
+
+    def __init__(self, source, coremodel, coreselection, grilo):
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._coreselection = coreselection
+        self._grilo = grilo
+        self._source = source
+        self._model = self._coremodel.props.playlists
+        self._model_filter = self._coremodel.props.playlists_filter
+        self._pls_todelete = []
+        self._tracker = TrackerWrapper().props.tracker
+
+        self._fast_options = Grl.OperationOptions()
+        self._fast_options.set_resolution_flags(
+            Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY)
+
+        self._initial_playlists_fill()
+
+    def _initial_playlists_fill(self):
+        args = {
+            "source": self._source,
+            "coreselection": self._coreselection,
+            "grilo": self._grilo
+        }
+
+        smart_playlists = {
+            "MostPlayed": MostPlayed(**args),
+            "NeverPlayed": NeverPlayed(**args),
+            "RecentlyPlayed": RecentlyPlayed(**args),
+            "RecentlyAdded": RecentlyAdded(**args),
+            "Favorites": Favorites(**args)
+        }
+
+        for playlist in smart_playlists.values():
+            self._model.append(playlist)
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(?playlist)
+            tracker:id(?playlist) AS ?id
+            nie:title(?playlist) AS ?title
+            tracker:added(?playlist) AS ?creation_date
+            nfo:entryCounter(?playlist) AS ?childcount
+        WHERE
+        {
+            ?playlist a nmm:Playlist .
+            OPTIONAL { ?playlist nie:url ?url;
+                       tracker:available ?available . }
+            FILTER ( !STRENDS(LCASE(?url), '.m3u')
+                     && !STRENDS(LCASE(?url), '.m3u8')
+                     && !STRENDS(LCASE(?url), '.pls')
+                     || !BOUND(nfo:belongsToContainer(?playlist)) )
+            FILTER ( !BOUND(?tag) )
+            OPTIONAL { ?playlist nao:hasTag ?tag }
+        }
+        """.replace('\n', ' ').strip()
+
+        options = self._fast_options.copy()
+
+        self._source.query(
+            query, self.METADATA_KEYS, options, self._add_user_playlist)
+
+    def _add_user_playlist(
+            self, source, op_id, media, remaining, data=None, error=None):
+        if error:
+            print("ERROR", error)
+            return
+        if not media:
+            self._coremodel.emit("playlists-loaded")
+            return
+
+        playlist = Playlist(
+            media=media, source=self._source, coremodel=self._coremodel,
+            coreselection=self._coreselection, grilo=self._grilo)
+
+        self._model.append(playlist)
+        callback = data
+        if callback is not None:
+            callback(playlist)
+
+    def _playlists_filter(self, playlist):
+        return playlist not in self._pls_todelete
+
+    def stage_playlist_deletion(self, playlist):
+        """Adds playlist to the list of playlists to delete
+
+        :param Playlist playlist: playlist
+        """
+        self._pls_todelete.append(playlist)
+        self._model_filter.set_filter_func(self._playlists_filter)
+
+    def finish_playlist_deletion(self, playlist, deleted):
+        """Removes playlist from the list of playlists to delete
+
+        :param Playlist playlist: playlist
+        :param bool deleted: indicates if the playlist has been deleted
+        """
+        self._pls_todelete.remove(playlist)
+        if deleted is False:
+            self._model_filter.set_filter_func(self._playlists_filter)
+            return
+
+        def _delete_cb(conn, res, data):
+            # FIXME: Check for failure.
+            conn.update_finish(res)
+            for idx, playlist_model in enumerate(self._model):
+                if playlist_model is playlist:
+                    self._model.remove(idx)
+                    break
+
+            self._model_filter.set_filter_func(self._playlists_filter)
+
+        query = """
+        DELETE {
+            ?playlist a rdfs:Resource .
+            ?entry a rdfs:Resource .
+
+        }
+        WHERE {
+            ?playlist a nmm:Playlist ;
+                      a nfo:MediaList .
+            OPTIONAL {
+                ?playlist nfo:hasMediaFileListEntry ?entry .
+            }
+            FILTER (
+            tracker:id(?playlist) = %(playlist_id)s
+            )
+        }
+        """.replace("\n", " ").strip() % {
+            "playlist_id": playlist.props.pl_id
+        }
+        self._tracker.update_async(
+            query, GLib.PRIORITY_LOW, None, _delete_cb, None)
+
+    def create_playlist(self, playlist_title, callback):
+        """Creates a new user playlist.
+
+        :param str playlist_title: playlist title
+        :param callback: function to perform once, the playlist is created
+        """
+        def _create_cb(conn, res, data):
+            result = conn.update_blank_finish(res)
+            playlist_urn = result[0][0]['playlist']
+            query = """
+            SELECT
+                rdf:type(?playlist)
+                tracker:id(?playlist) AS ?id
+                nie:title(?playlist) AS ?title
+                tracker:added(?playlist) AS ?creation_date
+                nfo:entryCounter(?playlist) AS ?childcount
+                WHERE
+                {
+                    ?playlist a nmm:Playlist .
+                    FILTER ( <%(playlist_urn)s> = ?playlist )
+                }
+            """.replace("\n", " ").strip() % {"playlist_urn": playlist_urn}
+
+            options = self._fast_options.copy()
+            self._source.query(
+                query, self.METADATA_KEYS, options, self._add_user_playlist,
+                callback)
+
+        query = """
+            INSERT {
+                _:playlist a nmm:Playlist ;
+                           a nfo:MediaList ;
+                             nie:title "%(title)s" ;
+                             nfo:entryCounter 0 .
+            }
+            """.replace("\n", " ").strip() % {"title": playlist_title}
+        self._tracker.update_blank_async(
+            query, GLib.PRIORITY_LOW, None, _create_cb, None)
+
+
+class Playlist(GObject.GObject):
+    """ Base class of all playlists """
+
+    __gsignals__ = {
+        "playlist-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()),
+    }
+
+    METADATA_KEYS = [
+        Grl.METADATA_KEY_ALBUM,
+        Grl.METADATA_KEY_ALBUM_ARTIST,
+        Grl.METADATA_KEY_ALBUM_DISC_NUMBER,
+        Grl.METADATA_KEY_ARTIST,
+        Grl.METADATA_KEY_CREATION_DATE,
+        Grl.METADATA_KEY_COMPOSER,
+        Grl.METADATA_KEY_DURATION,
+        Grl.METADATA_KEY_FAVOURITE,
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_PLAY_COUNT,
+        Grl.METADATA_KEY_THUMBNAIL,
+        Grl.METADATA_KEY_TITLE,
+        Grl.METADATA_KEY_TRACK_NUMBER,
+        Grl.METADATA_KEY_URL
+    ]
+
+    count = GObject.Property(type=int, default=0)
+    creation_date = GObject.Property(type=GLib.DateTime, default=None)
+    is_smart = GObject.Property(type=bool, default=False)
+    pl_id = GObject.Property(type=str, default=None)
+    query = GObject.Property(type=str, default=None)
+    tag_text = GObject.Property(type=str, default=None)
+    title = GObject.Property(type=str, default=None)
+
+    def __repr__(self):
+        return "<Playlist>"
+
+    def __init__(
+            self, media=None, query=None, tag_text=None, source=None,
+            coremodel=None, coreselection=None, grilo=None):
+        super().__init__()
+
+        if media:
+            self.props.pl_id = media.get_id()
+            self.props.title = utils.get_media_title(media)
+            self.props.count = media.get_childcount()
+            self.props.creation_date = media.get_creation_date()
+
+        self.props.query = query
+        self.props.tag_text = tag_text
+        self._model = None
+        self._source = source
+        self._coremodel = coremodel
+        self._coreselection = coreselection
+        self._grilo = grilo
+        self._tracker = TrackerWrapper().props.tracker
+
+        self._fast_options = Grl.OperationOptions()
+        self._fast_options.set_resolution_flags(
+            Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY)
+
+        self._songs_todelete = []
+
+    @GObject.Property(type=Gio.ListStore, default=None)
+    def model(self):
+        if self._model is None:
+            self._model = Gio.ListStore()
+
+            self._populate_model()
+
+        return self._model
+
+    @model.setter
+    def model(self, value):
+        self._model = value
+
+    def _populate_model(self):
+        query = """
+        SELECT
+            rdf:type(?song)
+            ?song AS ?tracker_urn
+            tracker:id(?entry) AS ?id
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            ?tag AS ?favourite
+            nie:contentAccessed(?song) AS ?last_played_time
+            nie:usageCounter(?song) AS ?play_count
+        WHERE {
+            ?playlist a nmm:Playlist ;
+                      a nfo:MediaList ;
+                        nfo:hasMediaFileListEntry ?entry .
+            ?entry a nfo:MediaFileListEntry ;
+                     nfo:entryUrl ?url .
+            ?song a nmm:MusicPiece ;
+                  a nfo:FileDataObject ;
+                    nie:url ?url .
+            OPTIONAL {
+                ?song nao:hasTag ?tag .
+                FILTER( ?tag = nao:predefined-tag-favorite )
+            }
+            FILTER (
+                %(filter_clause)s
+            )
+            FILTER (
+                NOT EXISTS { ?song a nmm:Video }
+                && NOT EXISTS { ?song a nmm:Playlist }
+            )
+        }
+        ORDER BY nfo:listPosition(?entry)
+        """.replace('\n', ' ').strip() % {
+            'filter_clause': 'tracker:id(?playlist) = ' + self.props.pl_id
+        }
+
+        def _add_to_playlist_cb(
+                source, op_id, media, remaining, user_data, error):
+            if not media:
+                self.props.count = self._model.get_n_items()
+                self.emit("playlist-loaded")
+                return
+
+            coresong = CoreSong(media, self._coreselection, self._grilo)
+            if coresong not in self._songs_todelete:
+                self._model.append(coresong)
+
+        options = Grl.OperationOptions()
+        options.set_resolution_flags(
+            Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY)
+
+        self._source.query(
+            query, self.METADATA_KEYS, options, _add_to_playlist_cb, None)
+
+    def rename(self, new_name):
+        """Rename a playlist
+
+        :param str new_name: new playlist name
+        """
+        def update_cb(conn, res, data):
+            # FIXME: Check for failure.
+            conn.update_finish(res)
+            # FIXME: Requery instead?
+            self.props.title = new_name
+
+        query = """
+        INSERT OR REPLACE {
+            ?playlist nie:title "%(title)s"
+        }
+        WHERE {
+            ?playlist a nmm:Playlist ;
+                      a nfo:MediaList .
+            OPTIONAL {
+                ?playlist nfo:hasMediaFileListEntry ?entry .
+            }
+            FILTER (
+                tracker:id(?playlist) = %(playlist_id)s
+            )
+        }
+        """.replace("\n", " ").strip() % {
+            'title': new_name,
+            'playlist_id': self.props.pl_id
+        }
+
+        self._tracker.update_async(
+            query, GLib.PRIORITY_LOW, None, update_cb, None)
+
+    def stage_song_deletion(self, coresong, index):
+        """Adds a song to the list of songs to delete
+
+        :param CoreSong coresong: song to delete
+        :param int position: Song position in the playlist
+        """
+        self._songs_todelete.append(coresong)
+        self._model.remove(index)
+        self.props.count -= 1
+
+    def undo_pending_song_deletion(self, coresong, position):
+        """Removes song from the list of songs to delete
+
+        :param CoreSong coresong: song to delete
+        :param int position: Song position in the playlist
+        """
+        self._songs_todelete.remove(coresong)
+        self._model.insert(position, coresong)
+        self.props.count += 1
+
+    def finish_song_deletion(self, coresong):
+        """Removes a song from the playlist
+
+        :param CoreSong coresong: song to remove
+        """
+
+        def update_cb(conn, res, data):
+            # FIXME: Check for failure.
+            conn.update_finish(res)
+
+        query = """
+        INSERT OR REPLACE {
+            ?entry nfo:listPosition ?position .
+
+        }
+        WHERE {
+            SELECT ?entry
+                   (?old_position - 1) AS ?position
+            WHERE {
+                ?entry a nfo:MediaFileListEntry ;
+                         nfo:listPosition ?old_position .
+                ?playlist nfo:hasMediaFileListEntry ?entry .
+                FILTER (?old_position > ?removed_position)
+                {
+                    SELECT ?playlist
+                           ?removed_position
+                    WHERE {
+                        ?playlist a nmm:Playlist ;
+                                  a nfo:MediaList ;
+                                    nfo:hasMediaFileListEntry ?removed_entry .
+                        ?removed_entry nfo:listPosition ?removed_position .
+                        FILTER (
+                            tracker:id(?playlist) = %(playlist_id)s &&
+                            tracker:id(?removed_entry) = %(song_id)s
+                        )
+                    }
+                }
+            }
+        }
+        INSERT OR REPLACE {
+            ?playlist nfo:entryCounter ?new_counter .
+        }
+        WHERE {
+            SELECT ?playlist
+                   (?counter - 1) AS ?new_counter
+            WHERE {
+                ?playlist a nmm:Playlist ;
+                          a nfo:MediaList ;
+                            nfo:entryCounter ?counter .
+                FILTER (
+                    tracker:id(?playlist) = %(playlist_id)s
+                )
+            }
+        }
+        DELETE {
+            ?playlist nfo:hasMediaFileListEntry ?entry .
+            ?entry a rdfs:Resource .
+        }
+        WHERE {
+            ?playlist a nmm:Playlist ;
+                      a nfo:MediaList ;
+                        nfo:hasMediaFileListEntry ?entry .
+            FILTER (
+                tracker:id(?playlist) = %(playlist_id)s &&
+                tracker:id(?entry) = %(song_id)s
+            )
+        }
+        """.replace("\n", " ").strip() % {
+            "playlist_id": self.props.pl_id,
+            "song_id": coresong.props.media.get_id()
+        }
+
+        self._tracker.update_async(
+            query, GLib.PRIORITY_LOW, None, update_cb, None)
+
+    def add_songs(self, coresongs):
+        """Adds songs to the playlist
+
+        :param Playlist playlist:
+        :param list coresongs: list of Coresong
+        """
+        def _add_to_model(source, op_id, media, remaining, error):
+            if not media:
+                self.props.count = self._model.get_n_items()
+                return
+
+            coresong = CoreSong(media, self._coreselection, self._grilo)
+            if coresong not in self._songs_todelete:
+                self._model.append(coresong)
+
+        def _requery_media(conn, res, coresong):
+            if self._model is None:
+                return
+
+            media_id = coresong.props.media.get_id()
+            query = """
+            SELECT
+                rdf:type(?song)
+                ?song AS ?tracker_urn
+                tracker:id(?entry) AS ?id
+                nie:url(?song) AS ?url
+                nie:title(?song) AS ?title
+                nmm:artistName(nmm:performer(?song)) AS ?artist
+                nie:title(nmm:musicAlbum(?song)) AS ?album
+                nfo:duration(?song) AS ?duration
+                ?tag AS ?favourite
+                nie:contentAccessed(?song) AS ?last_played_time
+                nie:usageCounter(?song) AS ?play_count
+            WHERE {
+                ?playlist a nmm:Playlist ;
+                          a nfo:MediaList ;
+                            nfo:hasMediaFileListEntry ?entry .
+                ?entry a nfo:MediaFileListEntry ;
+                         nfo:entryUrl ?url .
+                ?song a nmm:MusicPiece ;
+                      a nfo:FileDataObject ;
+                        nie:url ?url .
+                OPTIONAL {
+                    ?song nao:hasTag ?tag .
+                    FILTER( ?tag = nao:predefined-tag-favorite )
+                }
+                FILTER (
+                   %(filter_clause)s
+                )
+                FILTER (
+                    NOT EXISTS { ?song a nmm:Video }
+                    && NOT EXISTS { ?song a nmm:Playlist }
+                )
+            }
+            """.replace("\n", " ").strip() % {
+                "filter_clause": "tracker:id(?entry) = " + media_id}
+            options = self._fast_options.copy()
+            self._source.query(
+                query, self.METADATA_KEYS, options, _add_to_model)
+
+        for coresong in coresongs:
+            query = """
+            INSERT OR REPLACE {
+                _:entry a nfo:MediaFileListEntry ;
+                          nfo:entryUrl "%(song_uri)s" ;
+                          nfo:listPosition ?position .
+                ?playlist nfo:entryCounter ?position ;
+                          nfo:hasMediaFileListEntry _:entry .
+            }
+            WHERE {
+                SELECT ?playlist
+                       (?counter + 1) AS ?position
+                WHERE {
+                    ?playlist a nmm:Playlist ;
+                              a nfo:MediaList ;
+                                nfo:entryCounter ?counter .
+                    FILTER (
+                        tracker:id(?playlist) = %(playlist_id)s
+                    )
+                }
+            }
+            """.replace("\n", " ").strip() % {
+                "playlist_id": self.props.pl_id,
+                "song_uri": coresong.props.media.get_url()}
+
+            self._tracker.update_blank_async(
+                query, GLib.PRIORITY_LOW, None, _requery_media, coresong)
+
+    def reorder(self, previous_position, new_position):
+        """Changes the order of a songs in the playlist.
+
+        :param int previous_position: preivous song position
+        :param int new_position: new song position
+        """
+        def _position_changed_cb(conn, res, position):
+            # FIXME: Check for failure.
+            conn.update_finish(res)
+
+        coresong = self._model.get_item(previous_position)
+        self._model.remove(previous_position)
+        self._model.insert(new_position, coresong)
+
+        main_query = """
+        INSERT OR REPLACE {
+        ?entry
+            nfo:listPosition %(position)s
+        }
+        WHERE {
+            ?playlist a nmm:Playlist ;
+                      a nfo:MediaList ;
+                        nfo:hasMediaFileListEntry ?entry .
+            FILTER (
+                tracker:id(?playlist) = %(playlist_id)s &&
+                tracker:id(?entry) = %(song_id)s
+            )
+        }
+        """.replace("\n", " ").strip()
+
+        first_pos = min(previous_position, new_position)
+        last_pos = max(previous_position, new_position)
+
+        for position in range(first_pos, last_pos + 1):
+            coresong = self._model.get_item(position)
+            query = main_query % {
+                "playlist_id": self.props.pl_id,
+                "song_id": coresong.props.media.get_id(),
+                "position": position
+            }
+            self._tracker.update_async(
+                query, GLib.PRIORITY_LOW, None, _position_changed_cb,
+                position)
+
+
+class SmartPlaylist(Playlist):
+    """Base class for smart playlists"""
+
+    def __repr__(self):
+        return "<SmartPlaylist>"
+
+    def __init__(self, **args):
+        super().__init__(**args)
+
+        self.props.is_smart = True
+
+    @GObject.Property(type=Gio.ListStore, default=None)
+    def model(self):
+        if self._model is None:
+            self._model = Gio.ListStore.new(CoreSong)
+
+            def _add_to_model(source, op_id, media, remaining, error):
+                if error:
+                    print("ERROR", error)
+                    return
+
+                if not media:
+                    self.props.count = self._model.get_n_items()
+                    return
+
+                coresong = CoreSong(media, self._coreselection, self._grilo)
+                self._model.append(coresong)
+
+            options = self._fast_options.copy()
+
+            self._source.query(
+                self.props.query, self.METADATA_KEYS, options, _add_to_model)
+
+        return self._model
+
+
+class MostPlayed(SmartPlaylist):
+    """Most Played smart playlist"""
+
+    def __init__(self, **args):
+        super().__init__(**args)
+
+        self.props.tag_text = "MOST_PLAYED"
+        # TRANSLATORS: this is a playlist name
+        self.props.title = _("Most Played")
+        self.props.query = """
+        SELECT
+            rdf:type(?song)
+            tracker:id(?song) AS ?id
+            ?song AS ?tracker_urn
+            nie:title(?song) AS ?title
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nie:usageCounter(?song) AS ?play_count
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    nie:usageCounter ?count .
+            OPTIONAL { ?song nao:hasTag ?tag .
+                       FILTER (?tag = nao:predefined-tag-favorite) }
+        }
+        ORDER BY DESC(?count) LIMIT 50
+        """.replace('\n', ' ').strip()
+
+
+class NeverPlayed(SmartPlaylist):
+    """Never Played smart playlist"""
+
+    def __init__(self, **args):
+        super().__init__(**args)
+
+        self.props.tag_text = "NEVER_PLAYED"
+        # TRANSLATORS: this is a playlist name
+        self.props.title = _("Never Played")
+        self.props.query = """
+        SELECT
+            rdf:type(?song)
+            tracker:id(?song) AS ?id
+            ?song AS ?tracker_urn
+            nie:title(?song) AS ?title
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nie:usageCounter(?song) AS ?play_count
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+        WHERE {
+            ?song a nmm:MusicPiece ;
+            FILTER ( NOT EXISTS { ?song nie:usageCounter ?count .} )
+            OPTIONAL { ?song nao:hasTag ?tag .
+                       FILTER (?tag = nao:predefined-tag-favorite) }
+        } ORDER BY nfo:fileLastAccessed(?song) LIMIT 50
+        """.replace('\n', ' ').strip()
+
+
+class RecentlyPlayed(SmartPlaylist):
+    """Recently Played smart playlist"""
+
+    def __init__(self, **args):
+        super().__init__(**args)
+
+        self.props.tag_text = "RECENTLY_PLAYED"
+        # TRANSLATORS: this is a playlist name
+        self.props.title = _("Recently Played")
+
+        sparql_midnight_dateTime_format = "%Y-%m-%dT00:00:00Z"
+        days_difference = 7
+        seconds_difference = days_difference * 86400
+        compare_date = time.strftime(
+            sparql_midnight_dateTime_format,
+            time.gmtime(time.time() - seconds_difference))
+        self.props.query = """
+        SELECT
+            rdf:type(?song)
+            tracker:id(?song) AS ?id
+            ?song AS ?tracker_urn
+            nie:title(?song) AS ?title
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nie:usageCounter(?song) AS ?play_count
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    nie:contentAccessed ?last_played .
+            FILTER ( ?last_played > '%(compare_date)s'^^xsd:dateTime
+                     && EXISTS { ?song nie:usageCounter ?count .} )
+            OPTIONAL { ?song nao:hasTag ?tag .
+                       FILTER (?tag = nao:predefined-tag-favorite) }
+        } ORDER BY DESC(?last_played) LIMIT 50
+        """.replace('\n', ' ').strip() % {
+            'compare_date': compare_date
+        }
+
+
+class RecentlyAdded(SmartPlaylist):
+    """Recently Added smart playlist"""
+
+    def __init__(self, **args):
+        super().__init__(**args)
+
+        self.props.tag_text = "RECENTLY_ADDED"
+        # TRANSLATORS: this is a playlist name
+        self.props.title = _("Recently Added")
+
+        sparql_midnight_dateTime_format = "%Y-%m-%dT00:00:00Z"
+        days_difference = 7
+        seconds_difference = days_difference * 86400
+        compare_date = time.strftime(
+            sparql_midnight_dateTime_format,
+            time.gmtime(time.time() - seconds_difference))
+        self.props.query = """
+        SELECT
+            rdf:type(?song)
+            tracker:id(?song) AS ?id
+            ?song AS ?tracker_urn
+            nie:title(?song) AS ?title
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nie:usageCounter(?song) AS ?play_count
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    tracker:added ?added .
+            FILTER ( tracker:added(?song) > '%(compare_date)s'^^xsd:dateTime )
+            OPTIONAL { ?song nao:hasTag ?tag .
+                       FILTER (?tag = nao:predefined-tag-favorite) }
+        } ORDER BY DESC(tracker:added(?song)) LIMIT 50
+        """.replace('\n', ' ').strip() % {
+            'compare_date': compare_date,
+        }
+
+
+class Favorites(SmartPlaylist):
+    """Favorites smart playlist"""
+
+    def __init__(self, **args):
+        super().__init__(**args)
+
+        self.props.tag_text = "FAVORITES"
+        # TRANSLATORS: this is a playlist name
+        self.props.title = _("Favorite Songs")
+        self.props.query = """
+            SELECT
+                rdf:type(?song)
+                tracker:id(?song) AS ?id
+                ?song AS ?tracker_urn
+                nie:title(?song) AS ?title
+                nie:url(?song) AS ?url
+                nie:title(?song) AS ?title
+                nmm:artistName(nmm:performer(?song)) AS ?artist
+                nie:title(nmm:musicAlbum(?song)) AS ?album
+                nfo:duration(?song) AS ?duration
+                nie:usageCounter(?song) AS ?play_count
+                nmm:trackNumber(?song) AS ?track_number
+                nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+                nao:predefined-tag-favorite AS ?favourite
+            WHERE {
+                ?song a nmm:MusicPiece ;
+                        nie:isStoredAs ?as ;
+                        nao:hasTag nao:predefined-tag-favorite .
+                ?as nie:url ?url .
+            OPTIONAL { ?song nao:hasTag ?tag .
+                       FILTER (?tag = nao:predefined-tag-favorite) }
+
+            } ORDER BY DESC(tracker:added(?song))
+        """.replace('\n', ' ').strip()
diff --git a/gnomemusic/grilowrappers/grltrackerwrapper.py b/gnomemusic/grilowrappers/grltrackerwrapper.py
new file mode 100644
index 00000000..aaa9522c
--- /dev/null
+++ b/gnomemusic/grilowrappers/grltrackerwrapper.py
@@ -0,0 +1,878 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# code, but you are not obligated to do so.  If you do not wish to do so,
+# delete this exception statement from your version.
+
+import gi
+gi.require_versions({"Gfm": "0.1", "Grl": "0.3", 'Tracker': "2.0"})
+from gi.repository import Gfm, Grl, GLib, GObject, Tracker
+
+from gnomemusic.corealbum import CoreAlbum
+from gnomemusic.coreartist import CoreArtist
+from gnomemusic.coredisc import CoreDisc
+from gnomemusic.coresong import CoreSong
+from gnomemusic.grilowrappers.grltrackerplaylists import GrlTrackerPlaylists
+
+
+class GrlTrackerWrapper(GObject.GObject):
+    """Wrapper for the Grilo Tracker source.
+    """
+
+    METADATA_KEYS = [
+        Grl.METADATA_KEY_ALBUM,
+        Grl.METADATA_KEY_ALBUM_ARTIST,
+        Grl.METADATA_KEY_ALBUM_DISC_NUMBER,
+        Grl.METADATA_KEY_ARTIST,
+        Grl.METADATA_KEY_CREATION_DATE,
+        Grl.METADATA_KEY_COMPOSER,
+        Grl.METADATA_KEY_DURATION,
+        Grl.METADATA_KEY_FAVOURITE,
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_PLAY_COUNT,
+        Grl.METADATA_KEY_THUMBNAIL,
+        Grl.METADATA_KEY_TITLE,
+        Grl.METADATA_KEY_TRACK_NUMBER,
+        Grl.METADATA_KEY_URL
+    ]
+
+    METADATA_THUMBNAIL_KEYS = [
+        Grl.METADATA_KEY_ID,
+        Grl.METADATA_KEY_THUMBNAIL,
+    ]
+
+    def __repr__(self):
+        return "<GrlTrackerWrapper>"
+
+    def __init__(self, source, coremodel, coreselection, grilo):
+        """Initialize the Tracker wrapper
+
+        :param Grl.TrackerSource source: The Tracker source to wrap
+        :param CoreModel coremodel: CoreModel instance to use models
+        from
+        :param CoreSelection coreselection: CoreSelection instance to
+        use
+        :param CoreGrilo grilo: The CoreGrilo instance
+        """
+        super().__init__()
+
+        self._coremodel = coremodel
+        self._coreselection = coreselection
+        self._grilo = grilo
+        self._source = source
+        self._model = self._coremodel.props.songs
+        self._albums_model = self._coremodel.props.albums
+        self._album_ids = {}
+        self._artists_model = self._coremodel.props.artists
+        self._artist_ids = {}
+        self._hash = {}
+        self._song_search_proxy = self._coremodel.props.songs_search_proxy
+        self._album_search_model = self._coremodel.props.albums_search
+        self._artist_search_model = self._coremodel.props.artists_search
+
+        self._song_search_tracker = Gfm.FilterListModel.new(self._model)
+        self._song_search_tracker.set_filter_func(lambda a: False)
+        self._song_search_proxy.append(self._song_search_tracker)
+
+        self._fast_options = Grl.OperationOptions()
+        self._fast_options.set_resolution_flags(
+            Grl.ResolutionFlags.FAST_ONLY | Grl.ResolutionFlags.IDLE_RELAY)
+
+        self._initial_songs_fill(self._source)
+        self._initial_albums_fill(self._source)
+        self._initial_artists_fill(self._source)
+
+        self._tracker_playlists = GrlTrackerPlaylists(
+            source, coremodel, coreselection, grilo)
+
+        self._source.notify_change_start()
+        self._source.connect("content-changed", self._on_content_changed)
+
+    @GObject.Property(
+        type=Grl.Source, default=None, flags=GObject.ParamFlags.READABLE)
+    def source(self):
+        return self._source
+
+    @staticmethod
+    def _location_filter():
+        try:
+            music_dir = GLib.get_user_special_dir(
+                GLib.UserDirectory.DIRECTORY_MUSIC)
+            assert music_dir is not None
+        except (TypeError, AssertionError):
+            print("XDG Music dir is not set")
+            return
+
+        music_dir = Tracker.sparql_escape_string(
+            GLib.filename_to_uri(music_dir))
+
+        query = """
+        FILTER (STRSTARTS(nie:url(?song), '%(music_dir)s/'))
+        """.replace('\n', ' ').strip() % {
+            'music_dir': music_dir
+        }
+
+        return query
+
+    def _on_content_changed(self, source, medias, change_type, loc_unknown):
+        for media in medias:
+            if change_type == Grl.SourceChangeType.ADDED:
+                print("ADDED", media.get_id())
+                self._add_media(media)
+                self._check_album_change(media)
+                self._check_artist_change(media)
+            elif change_type == Grl.SourceChangeType.CHANGED:
+                print("CHANGED", media.get_id())
+                self._changed_media(media)
+            elif change_type == Grl.SourceChangeType.REMOVED:
+                print("REMOVED", media.get_id())
+                self._remove_media(media)
+                self._check_album_change(media)
+                self._check_artist_change(media)
+
+    def _check_album_change(self, media):
+        album_ids = {}
+
+        query = """
+        SELECT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            nie:title(?album) AS ?title
+            ?composer AS ?composer
+            ?album_artist AS ?album_artist
+            nmm:artistName(?performer) AS ?artist
+            YEAR(MAX(nie:contentCreated(?song))) AS ?creation_date
+        WHERE {
+            ?album a nmm:MusicAlbum .
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?performer .
+            OPTIONAL { ?song nmm:composer/nmm:artistName ?composer . }
+            OPTIONAL { ?album nmm:albumArtist/nmm:artistName ?album_artist . }
+            %(location_filter)s
+        } GROUP BY ?album
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter()
+        }
+
+        def check_album_cb(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                changed_ids = set(
+                    album_ids.keys()) ^ set(self._album_ids.keys())
+                print("ALBUMS CHANGED", changed_ids)
+
+                for key in changed_ids:
+                    if key in album_ids:
+                        self._albums_model.append(album_ids[key])
+                    elif key in self._album_ids:
+                        for idx, corealbum in enumerate(self._albums_model):
+                            if corealbum.media.get_id() == key:
+                                self._albums_model.remove(idx)
+                                break
+
+                self._album_ids = album_ids
+                return
+
+            album = CoreAlbum(media, self._coremodel)
+            album_ids[media.get_id()] = album
+
+        options = self._fast_options.copy()
+
+        self._source.query(
+            query, self.METADATA_KEYS, options, check_album_cb)
+
+    def _check_artist_change(self, media):
+        artist_ids = {}
+
+        query = """
+        SELECT
+            rdf:type(?artist_class)
+            tracker:id(?artist_class) AS ?id
+            nmm:artistName(?artist_class) AS ?artist
+        WHERE {
+            ?artist_class a nmm:Artist .
+            ?song a nmm:MusicPiece;
+                    nmm:musicAlbum ?album;
+                    nmm:performer ?artist_class .
+            %(location_filter)s
+        } GROUP BY ?artist_class
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter()
+        }
+
+        def check_artist_cb(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                changed_ids = set(
+                    artist_ids.keys()) ^ set(self._artist_ids.keys())
+                print("ARTISTS CHANGED", changed_ids)
+
+                for key in changed_ids:
+                    if key in artist_ids:
+                        self._artists_model.append(artist_ids[key])
+                    elif key in self._artist_ids:
+                        for idx, coreartist in enumerate(self._artists_model):
+                            if coreartist.media.get_id() == key:
+                                self._artists_model.remove(idx)
+                                break
+
+                self._artist_ids = artist_ids
+                return
+
+            artist = CoreArtist(media, self._coremodel)
+            artist_ids[media.get_id()] = artist
+
+        options = self._fast_options.copy()
+
+        self._source.query(
+            query, self.METADATA_KEYS, options, check_artist_cb)
+
+    def _remove_media(self, media):
+        try:
+            coresong = self._hash.pop(media.get_id())
+        except KeyError:
+            print("Removal KeyError")
+            return
+
+        for idx, coresong_model in enumerate(self._model):
+            if coresong_model is coresong:
+                print(
+                    "removing", coresong.props.media.get_id(),
+                    coresong.props.title)
+                self._model.remove(idx)
+                break
+
+    def _song_media_query(self, media_id):
+        query = """
+        SELECT DISTINCT
+            rdf:type(?song)
+            ?song AS ?tracker_urn
+            nie:title(?song) AS ?title
+            tracker:id(?song) AS ?id
+            ?song
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nie:usageCounter(?song) AS ?play_count
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+        WHERE {
+            ?song a nmm:MusicPiece .
+            OPTIONAL {
+                ?song nao:hasTag ?tag .
+                FILTER (?tag = nao:predefined-tag-favorite)
+            }
+            FILTER ( tracker:id(?song) = %(media_id)s )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter(),
+            'media_id': media_id
+        }
+
+        return query
+
+    def _add_media(self, media):
+
+        def _add_media(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                return
+
+            # FIXME: Figure out why we get double additions.
+            if media.get_id() in self._hash.keys():
+                print("ALREADY ADDED")
+                return
+
+            song = CoreSong(media, self._coreselection, self._grilo)
+            self._model.append(song)
+            self._hash[media.get_id()] = song
+
+            print("UPDATE ID", media.get_id(), media.get_title())
+
+        options = self._fast_options.copy()
+
+        self._source.query(
+            self._song_media_query(media.get_id()), self.METADATA_KEYS,
+            options, _add_media)
+
+    def _changed_media(self, media):
+
+        def _update_changed_media(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                return
+
+            self._hash[media.get_id()].update(media)
+
+        options = self._fast_options.copy()
+
+        self._source.query(
+            self._song_media_query(media.get_id()), self.METADATA_KEYS,
+            options, _update_changed_media)
+
+    def _initial_songs_fill(self, source):
+
+        def _add_to_model(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                return
+
+            song = CoreSong(media, self._coreselection, self._grilo)
+            self._model.append(song)
+            self._hash[media.get_id()] = song
+
+        query = """
+        SELECT
+            rdf:type(?song)
+            ?song AS ?tracker_urn
+            nie:title(?song) AS ?title
+            tracker:id(?song) AS ?id
+            ?song
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nie:usageCounter(?song) AS ?play_count
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+        WHERE {
+            ?song a nmm:MusicPiece .
+            OPTIONAL {
+                ?song nao:hasTag ?tag .
+                FILTER (?tag = nao:predefined-tag-favorite)
+            }
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter()
+        }
+
+        options = self._fast_options.copy()
+        self._source.query(query, self.METADATA_KEYS, options, _add_to_model)
+
+    def _initial_albums_fill(self, source):
+
+        def _add_to_albums_model(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                return
+
+            album = CoreAlbum(media, self._coremodel)
+            self._albums_model.append(album)
+            self._album_ids[media.get_id()] = album
+
+        query = """
+        SELECT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            nie:title(?album) AS ?title
+            ?composer AS ?composer
+            ?album_artist AS ?album_artist
+            nmm:artistName(?performer) AS ?artist
+            YEAR(MAX(nie:contentCreated(?song))) AS ?creation_date
+        WHERE
+        {
+            ?album a nmm:MusicAlbum .
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?performer .
+            OPTIONAL { ?song nmm:composer/nmm:artistName ?composer . }
+            OPTIONAL { ?album nmm:albumArtist/nmm:artistName ?album_artist . }
+            %(location_filter)s
+        } GROUP BY ?album
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter()
+        }
+
+        options = self._fast_options.copy()
+
+        source.query(query, self.METADATA_KEYS, options, _add_to_albums_model)
+
+    def _initial_artists_fill(self, source):
+
+        def _add_to_artists_model(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                self._coremodel.emit("artists-loaded")
+                return
+
+            artist = CoreArtist(media, self._coremodel)
+            self._artists_model.append(artist)
+            self._artist_ids[media.get_id()] = artist
+
+        query = """
+        SELECT
+            rdf:type(?artist)
+            tracker:id(?artist) AS ?id
+            nmm:artistName(?artist) AS ?artist
+        WHERE {
+            ?artist a nmm:Artist .
+            ?song a nmm:MusicPiece;
+                    nmm:musicAlbum ?album;
+                    nmm:performer ?artist .
+            %(location_filter)s
+        } GROUP BY ?artist
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter()
+        }
+
+        options = self._fast_options.copy()
+
+        source.query(
+            query, [Grl.METADATA_KEY_ARTIST], options, _add_to_artists_model)
+
+    def get_artist_albums(self, media, model):
+        """Get all albums by an artist
+
+        :param Grl.Media media: The media with the artist id
+        :param Dazzle.ListModelFilter model: The model to fill
+        """
+        artist_id = media.get_id()
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            nie:title(?album) AS ?title
+        WHERE {
+            ?album a nmm:MusicAlbum .
+            OPTIONAL { ?album  nmm:albumArtist ?album_artist . }
+            ?song a nmm:MusicPiece;
+                    nmm:musicAlbum ?album;
+                    nmm:performer ?artist .
+            FILTER ( tracker:id(?album_artist) = %(artist_id)s
+                     || tracker:id(?artist) = %(artist_id)s )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'artist_id': int(artist_id),
+            'location_filter': self._location_filter()
+        }
+
+        albums = []
+
+        def query_cb(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                model.set_filter_func(albums_filter, albums)
+                return
+
+            albums.append(media)
+
+        def albums_filter(corealbum, albums):
+            for media in albums:
+                if media.get_id() == corealbum.props.media.get_id():
+                    return True
+
+            return False
+
+        options = self._fast_options.copy()
+        self._source.query(
+            query, [Grl.METADATA_KEY_TITLE], options, query_cb)
+
+    def get_album_discs(self, media, disc_model):
+        """Get all discs of an album
+
+        :param Grl.Media media: The media with the album id
+        :param Gfm.SortListModel disc_model: The model to fill
+        """
+        album_id = media.get_id()
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(?song)
+            tracker:id(?album) AS ?id
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) as ?album_disc_number
+        WHERE {
+            ?song a nmm:MusicPiece;
+                    nmm:musicAlbum ?album .
+            FILTER ( tracker:id(?album) = %(album_id)s )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'album_id': int(album_id),
+            'location_filter': self._location_filter()
+        }
+
+        def _disc_nr_cb(source, op_id, media, user_data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                return
+
+            disc_nr = media.get_album_disc_number()
+            coredisc = CoreDisc(media, disc_nr, self._coremodel)
+            disc_model.append(coredisc)
+
+        options = self._fast_options.copy()
+        self._source.query(
+            query, [Grl.METADATA_KEY_ALBUM_DISC_NUMBER], options, _disc_nr_cb)
+
+    def populate_album_disc_songs(self, media, disc_nr, callback):
+        # FIXME: Pass a model and fill it.
+        # FIXME: The query is similar to the other song queries, reuse
+        # if possible.
+        """Get all songs of an album disc
+
+        :param Grl.Media media: The media with the album id
+        :param int disc_nr: The disc number
+        :param callback: The callback to call for every song added
+        """
+        album_id = media.get_id()
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(?song)
+            ?song AS ?tracker_urn
+            tracker:id(?song) AS ?id
+            nie:url(?song) AS ?url
+            nie:title(?song) AS ?title
+            nmm:artistName(nmm:performer(?song)) AS ?artist
+            nie:title(nmm:musicAlbum(?song)) AS ?album
+            nfo:duration(?song) AS ?duration
+            nmm:trackNumber(?song) AS ?track_number
+            nmm:setNumber(nmm:musicAlbumDisc(?song)) AS ?album_disc_number
+            ?tag AS ?favourite
+            nie:usageCounter(?song) AS ?play_count
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album .
+            OPTIONAL { ?song nao:hasTag ?tag .
+                       FILTER (?tag = nao:predefined-tag-favorite) } .
+            FILTER ( tracker:id(?album) = %(album_id)s
+                     && nmm:setNumber(nmm:musicAlbumDisc(?song)) = %(disc_nr)s
+            )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'album_id': album_id,
+            'disc_nr': disc_nr,
+            'location_filter': self._location_filter()
+        }
+
+        options = self._fast_options.copy()
+        self._source.query(query, self.METADATA_KEYS, options, callback)
+
+    def search(self, text):
+        term = Tracker.sparql_escape_string(
+            GLib.utf8_normalize(
+                GLib.utf8_casefold(text, -1), -1, GLib.NormalizeMode.NFKD))
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(?song)
+            tracker:id(?song) AS ?id
+        WHERE {
+            ?song a nmm:MusicPiece .
+            BIND(tracker:normalize(
+                nie:title(nmm:musicAlbum(?song)), 'nfkd') AS ?match1) .
+            BIND(tracker:normalize(
+                nmm:artistName(nmm:performer(?song)), 'nfkd') AS ?match2) .
+            BIND(tracker:normalize(
+                nie:title(?song), 'nfkd') AS ?match3) .
+            BIND(
+                tracker:normalize(nmm:composer(?song), 'nfkd') AS ?match4) .
+            FILTER (
+                CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match1)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match1), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match2)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match2), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match3)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match3), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match4)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match4), "%(name)s")
+            )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter(),
+            'name': term
+        }
+
+        filter_ids = []
+
+        def songs_filter(coresong):
+            return coresong.media.get_id() in filter_ids
+
+        def songs_search_cb(source, op_id, media, data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                self._song_search_tracker.set_filter_func(songs_filter)
+                return
+
+            filter_ids.append(media.get_id())
+
+        options = self._fast_options.copy()
+
+        self._source.query(query, self.METADATA_KEYS, options, songs_search_cb)
+
+        # Album search
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(nmm:musicAlbum(?song))
+            tracker:id(nmm:musicAlbum(?song)) AS ?id
+        WHERE {
+            ?song a nmm:MusicPiece .
+            BIND(tracker:normalize(
+                nie:title(nmm:musicAlbum(?song)), 'nfkd') AS ?match1) .
+            BIND(tracker:normalize(
+                nmm:artistName(nmm:performer(?song)), 'nfkd') AS ?match2) .
+            BIND(tracker:normalize(nie:title(?song), 'nfkd') AS ?match3) .
+            BIND(tracker:normalize(nmm:composer(?song), 'nfkd') AS ?match4) .
+            FILTER (
+                CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match1)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match1), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match2)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match2), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match3)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match3), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match4)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match4), "%(name)s")
+            )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter(),
+            'name': term
+        }
+
+        album_filter_ids = []
+
+        def album_filter(corealbum):
+            return corealbum.media.get_id() in album_filter_ids
+
+        def albums_search_cb(source, op_id, media, data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                self._album_search_model.set_filter_func(album_filter)
+                return
+
+            album_filter_ids.append(media.get_id())
+
+        options = self._fast_options.copy()
+        self._source.query(
+            query, self.METADATA_KEYS, options, albums_search_cb)
+
+        # Artist search
+
+        query = """
+        SELECT DISTINCT
+            rdf:type(?artist)
+            tracker:id(?artist) AS ?id
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?artist .
+            BIND(tracker:normalize(
+                nie:title(nmm:musicAlbum(?song)), 'nfkd') AS ?match1) .
+            BIND(tracker:normalize(
+                nmm:artistName(nmm:performer(?song)), 'nfkd') AS ?match2) .
+            BIND(tracker:normalize(nie:title(?song), 'nfkd') AS ?match3) .
+            BIND(tracker:normalize(nmm:composer(?song), 'nfkd') AS ?match4) .
+            FILTER (
+                CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match1)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match1), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match2)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match2), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match3)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match3), "%(name)s")
+                || CONTAINS(tracker:case-fold(
+                    tracker:unaccent(?match4)), "%(name)s")
+                || CONTAINS(tracker:case-fold(?match4), "%(name)s")
+            )
+            %(location_filter)s
+        }
+        """.replace('\n', ' ').strip() % {
+            'location_filter': self._location_filter(),
+            'name': term
+        }
+
+        artist_filter_ids = []
+
+        def artist_filter(coreartist):
+            return coreartist.media.get_id() in artist_filter_ids
+
+        def artist_search_cb(source, op_id, media, data, error):
+            if error:
+                print("ERROR", error)
+                return
+
+            if not media:
+                self._artist_search_model.set_filter_func(artist_filter)
+                return
+
+            artist_filter_ids.append(media.get_id())
+
+        options = self._fast_options.copy()
+        self._source.query(
+            query, self.METADATA_KEYS, options, artist_search_cb)
+
+    def get_album_art_for_item(self, coresong, callback):
+        """Placeholder until we got a better solution
+        """
+        item_id = coresong.props.media.get_id()
+
+        if coresong.props.media.is_audio():
+            query = self._get_album_for_song_id(item_id)
+        else:
+            query = self._get_album_for_album_id(item_id)
+
+        full_options = Grl.OperationOptions()
+        full_options.set_resolution_flags(
+            Grl.ResolutionFlags.FULL
+            | Grl.ResolutionFlags.IDLE_RELAY)
+        full_options.set_count(1)
+
+        self.search_source.query(
+            query, self.METADATA_THUMBNAIL_KEYS, full_options, callback)
+
+    def _get_album_for_album_id(self, album_id):
+        # Even though we check for the album_artist, we fill
+        # the artist key, since Grilo coverart plugins use
+        # only that key for retrieval.
+        query = """
+        SELECT DISTINCT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            tracker:coalesce(nmm:artistName(?album_artist),
+                             nmm:artistName(?song_artist)) AS ?artist
+            nie:title(?album) AS ?album
+        WHERE {
+            ?album a nmm:MusicAlbum .
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?song_artist .
+            OPTIONAL { ?album nmm:albumArtist ?album_artist . }
+            FILTER (
+                tracker:id(?album) = %(album_id)s
+            )
+            %(location_filter)s
+        }
+        """.replace("\n", " ").strip() % {
+                'album_id': album_id,
+                'location_filter': self._location_filter()
+        }
+
+        return query
+
+    def _get_album_for_song_id(self, song_id):
+        # See get_album_for_album_id comment.
+        query = """
+        SELECT DISTINCT
+            rdf:type(?album)
+            tracker:id(?album) AS ?id
+            tracker:coalesce(nmm:artistName(?album_artist),
+                             nmm:artistName(?song_artist)) AS ?artist
+            nie:title(?album) AS ?album
+        WHERE {
+            ?song a nmm:MusicPiece ;
+                    nmm:musicAlbum ?album ;
+                    nmm:performer ?song_artist .
+            OPTIONAL { ?album nmm:albumArtist ?album_artist . }
+            FILTER (
+                tracker:id(?song) = %(song_id)s
+            )
+            FILTER (
+                NOT EXISTS { ?song a nmm:Video }
+                && NOT EXISTS { ?song a nmm:Playlist }
+            )
+            %(location_filter)s
+        }
+        """.replace("\n", " ").strip() % {
+            'location_filter': self._location_filter(),
+            'song_id': song_id
+        }
+
+        return query
+
+    def stage_playlist_deletion(self, playlist):
+        """Prepares playlist deletion.
+
+        :param Playlist playlist: playlist
+        """
+        self._tracker_playlists.stage_playlist_deletion(playlist)
+
+    def finish_playlist_deletion(self, playlist, deleted):
+        """Finishes playlist deletion.
+
+        :param Playlist playlist: playlist
+        :param bool deleted: indicates if the playlist has been deleted
+        """
+        self._tracker_playlists.finish_playlist_deletion(playlist, deleted)
+
+    def create_playlist(self, playlist_title, callback):
+        """Creates a new user playlist.
+
+        :param str playlist_title: playlist title
+        :param callback: function to perform once, the playlist is created
+        """
+        self._tracker_playlists.create_playlist(playlist_title, callback)
diff --git a/gnomemusic/gstplayer.py b/gnomemusic/gstplayer.py
index 12d300b7..0c4591f6 100644
--- a/gnomemusic/gstplayer.py
+++ b/gnomemusic/gstplayer.py
@@ -54,6 +54,7 @@ class GstPlayer(GObject.GObject):
         "about-to-finish": (GObject.SignalFlags.RUN_FIRST, None, ()),
         "clock-tick": (GObject.SignalFlags.RUN_FIRST, None, (int, )),
         'eos': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "error": (GObject.SignalFlags.RUN_FIRST, None, ()),
         'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, ()),
         "stream-start": (GObject.SignalFlags.RUN_FIRST, None, ())
     }
@@ -196,7 +197,7 @@ class GstPlayer(GObject.GObject):
                 message.src.get_name(), error.message))
         logger.warning("Debugging info:\n{}".format(debug))
 
-        self.emit('eos')
+        self.emit("error")
         return True
 
     @log
diff --git a/gnomemusic/mpris.py b/gnomemusic/mpris.py
index ae42cb17..8cd51642 100644
--- a/gnomemusic/mpris.py
+++ b/gnomemusic/mpris.py
@@ -22,6 +22,7 @@
 # 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 itertools import chain
 import logging
 import re
 
@@ -31,8 +32,6 @@ from gnomemusic import log
 from gnomemusic.albumartcache import lookup_art_file_from_cache
 from gnomemusic.gstplayer import Playback
 from gnomemusic.player import PlayerPlaylist, RepeatMode
-from gnomemusic.playlists import Playlists
-import gnomemusic.utils as utils
 
 logger = logging.getLogger(__name__)
 
@@ -272,6 +271,8 @@ class MPRIS(DBusInterface):
     MEDIA_PLAYER2_TRACKLIST_IFACE = 'org.mpris.MediaPlayer2.TrackList'
     MEDIA_PLAYER2_PLAYLISTS_IFACE = 'org.mpris.MediaPlayer2.Playlists'
 
+    _playlist_nb_songs = 10
+
     def __repr__(self):
         return "<MPRIS>"
 
@@ -291,10 +292,12 @@ class MPRIS(DBusInterface):
         self._player.connect(
             'playlist-changed', self._on_player_playlist_changed)
 
-        self._playlists = Playlists.get_default()
-        self._playlists_model = None
-        self._playlists.connect('playlist-renamed', self._on_playlist_renamed)
-        self._playlists.connect("notify::ready", self._on_playlists_loading)
+        self._coremodel = app.props.coremodel
+        self._player_model = self._coremodel.props.playlist_sort
+
+        self._playlists_model = self._coremodel.props.playlists_sort
+        self._playlists_loaded_id = self._coremodel.connect(
+            "playlists-loaded", self._on_playlists_loaded)
 
         self._player_previous_type = None
         self._path_list = []
@@ -306,6 +309,7 @@ class MPRIS(DBusInterface):
         self._previous_loop_status = ""
         self._previous_mpris_playlist = self._get_active_playlist()
         self._previous_playback_status = "Stopped"
+        self._previous_playlist_count = 0
 
     @log
     def _get_playback_status(self):
@@ -327,42 +331,42 @@ class MPRIS(DBusInterface):
             return 'Playlist'
 
     @log
-    def _get_metadata(self, media=None, index=None):
-        song_dbus_path = self._get_song_dbus_path(media, index)
+    def _get_metadata(self, coresong=None, index=None):
+        song_dbus_path = self._get_song_dbus_path(coresong, index)
         if not self._player.props.current_song:
             return {
                 'mpris:trackid': GLib.Variant('o', song_dbus_path)
             }
 
-        if not media:
-            media = self._player.props.current_song
+        if not coresong:
+            coresong = self._player.props.current_song
 
-        length = media.get_duration() * 1e6
-        user_rating = 1.0 if media.get_favourite() else 0.0
-        artist = utils.get_artist_name(media)
+        length = coresong.props.duration * 1e6
+        user_rating = 1.0 if coresong.props.favorite else 0.0
+        artist = coresong.props.artist
 
         metadata = {
             'mpris:trackid': GLib.Variant('o', song_dbus_path),
-            'xesam:url': GLib.Variant('s', media.get_url()),
+            'xesam:url': GLib.Variant('s', coresong.props.url),
             'mpris:length': GLib.Variant('x', length),
-            'xesam:useCount': GLib.Variant('i', media.get_play_count()),
+            'xesam:useCount': GLib.Variant('i', coresong.props.play_count),
             'xesam:userRating': GLib.Variant('d', user_rating),
-            'xesam:title': GLib.Variant('s', utils.get_media_title(media)),
-            'xesam:album': GLib.Variant('s', utils.get_album_title(media)),
+            'xesam:title': GLib.Variant('s', coresong.props.title),
+            'xesam:album': GLib.Variant('s', coresong.props.album),
             'xesam:artist': GLib.Variant('as', [artist]),
             'xesam:albumArtist': GLib.Variant('as', [artist])
         }
 
-        genre = media.get_genre()
+        genre = coresong.props.media.get_genre()
         if genre is not None:
             metadata['xesam:genre'] = GLib.Variant('as', [genre])
 
-        last_played = media.get_last_played()
+        last_played = coresong.props.media.get_last_played()
         if last_played is not None:
             last_played_str = last_played.format("%FT%T%:z")
             metadata['xesam:lastUsed'] = GLib.Variant('s', last_played_str)
 
-        track_nr = media.get_track_number()
+        track_nr = coresong.props.track_number
         if track_nr > 0:
             metadata['xesam:trackNumber'] = GLib.Variant('i', track_nr)
 
@@ -373,12 +377,12 @@ class MPRIS(DBusInterface):
         # loading.
         # FIXME: The thumbnail retrieval should take place in the
         # player.
-        art_url = media.get_thumbnail()
+        art_url = coresong.props.media.get_thumbnail()
         if not art_url:
-            thumb_file = lookup_art_file_from_cache(media)
+            thumb_file = lookup_art_file_from_cache(coresong)
             if thumb_file:
                 art_url = GLib.filename_to_uri(thumb_file.get_path())
-                media.set_thumbnail(art_url)
+                coresong.props.media.set_thumbnail(art_url)
 
         if art_url:
             metadata['mpris:artUrl'] = GLib.Variant('s', art_url)
@@ -386,15 +390,16 @@ class MPRIS(DBusInterface):
         return metadata
 
     @log
-    def _get_song_dbus_path(self, media=None, index=None):
+    def _get_song_dbus_path(self, coresong=None, index=None):
         """Convert a Grilo media to a D-Bus path
 
-        The hex encoding is used to remove any possible invalid character.
-        Use player index to make the path truly unique in case the same song
-        is present multiple times in a playlist.
-        If media is None, it means that the current song path is requested.
+        The hex encoding is used to remove any possible invalid
+        character. Use player index to make the path truly unique in
+        case the same song is present multiple times in a playlist.
+        If coresong is None, it means that the current song path is
+        requested.
 
-        :param Grl.Media media: The media object
+        :param CoreSong coresong: The CoreSong object
         :param int index: The media position in the current playlist
         :return: a D-Bus id to uniquely identify the song
         :rtype: str
@@ -402,11 +407,11 @@ class MPRIS(DBusInterface):
         if not self._player.props.current_song:
             return "/org/mpris/MediaPlayer2/TrackList/NoTrack"
 
-        if not media:
-            media = self._player.props.current_song
-            index = self._player.props.current_song_index
+        if not coresong:
+            coresong = self._player.props.current_song
+            index = self._player.props.position
 
-        id_hex = media.get_id().encode('ascii').hex()
+        id_hex = coresong.props.grlid.encode('ascii').hex()
         path = "/org/gnome/GnomeMusic/TrackList/{}_{}".format(
             id_hex, index)
         return path
@@ -416,9 +421,37 @@ class MPRIS(DBusInterface):
         previous_path_list = self._path_list
         self._path_list = []
         self._metadata_list = []
-        for index, song in self._player.get_mpris_playlist():
-            path = self._get_song_dbus_path(song, index)
-            metadata = self._get_metadata(song, index)
+        current_position = self._player.props.position
+        nb_songs = self._player_model.get_n_items()
+
+        index_min = current_position - self._playlist_nb_songs
+        index_max = current_position + self._playlist_nb_songs + 1
+        if self._player.get_playlist_type() == PlayerPlaylist.Type.ALBUM:
+            index_min = 0
+            index_max = self._player_model.get_n_items()
+
+        first_index = max(index_min, 0)
+        last_index = min(index_max, nb_songs)
+        positions = range(first_index, last_index)
+
+        nb_songs_max = 2 * self._playlist_nb_songs + 1
+        if (self._player.props.repeat_mode == RepeatMode.ALL
+                and (last_index - first_index) < nb_songs_max):
+            offset_sup = min(
+                self._playlist_nb_songs - last_index + current_position + 1,
+                first_index)
+            offset_inf = min(
+                self._playlist_nb_songs - current_position + first_index,
+                nb_songs - last_index)
+
+            positions = chain(
+                range(nb_songs - offset_inf, nb_songs), positions,
+                range(offset_sup))
+
+        for position in positions:
+            coresong = self._player_model.get_item(position)
+            path = self._get_song_dbus_path(coresong, position)
+            metadata = self._get_metadata(coresong, position)
             self._path_list.append(path)
             self._metadata_list.append(metadata)
 
@@ -437,8 +470,9 @@ class MPRIS(DBusInterface):
         :return: a D-Bus id to uniquely identify the playlist
         :rtype: str
         """
+        # Smart Playlists do not have an id
         if playlist:
-            pl_id = playlist.props.pl_id
+            pl_id = playlist.props.pl_id or playlist.props.tag_text
         else:
             pl_id = "Invalid"
 
@@ -554,26 +588,34 @@ class MPRIS(DBusInterface):
         self._properties_changed(
             MPRIS.MEDIA_PLAYER2_PLAYLISTS_IFACE, properties, [])
 
-    def _on_playlists_loading(self, klass, param):
-        if not self._playlists.props.ready:
-            return
+    def _on_playlists_loaded(self, klass):
+        self._coremodel.disconnect(self._playlists_loaded_id)
+        for playlist in self._playlists_model:
+            playlist.connect("notify::title", self._on_playlist_renamed)
 
-        self._playlists_model = self._playlists.get_playlists_model()
         self._playlists_model.connect(
             "items-changed", self._on_playlists_count_changed)
         self._on_playlists_count_changed(None, None, 0, 0)
 
     @log
-    def _on_playlists_count_changed(self, klass, position, removed, added):
+    def _on_playlists_count_changed(self, model, position, removed, added):
         playlist_count = self._playlists_model.get_n_items()
+        if playlist_count == self._previous_playlist_count:
+            return
+
+        self._previous_playlist_count = playlist_count
         properties = {"PlaylistCount": GLib.Variant("u", playlist_count)}
         self._properties_changed(
             MPRIS.MEDIA_PLAYER2_PLAYLISTS_IFACE, properties, [])
 
+        if added == 0:
+            return
+
+        model[position].connect("notify::title", self._on_playlist_renamed)
+
     @log
-    def _on_playlist_renamed(self, playlists, renamed_playlist):
-        mpris_playlist = self._get_mpris_playlist_from_playlist(
-            renamed_playlist)
+    def _on_playlist_renamed(self, playlist, param):
+        mpris_playlist = self._get_mpris_playlist_from_playlist(playlist)
         self._dbus_emit_signal('PlaylistChanged', {'Playlist': mpris_playlist})
 
     def _raise(self):
@@ -694,11 +736,13 @@ class MPRIS(DBusInterface):
 
         :param str path: Identifier of the track to skip to
         """
-        current_song_path = self._get_song_dbus_path()
-        current_song_index = self._path_list.index(current_song_path)
-        goto_index = self._path_list.index(path)
-        song_offset = goto_index - current_song_index
-        self._player.play(song_offset=song_offset)
+        # FIXME: Dropped this for core rewrite.
+        pass
+        # current_song_path = self._get_song_dbus_path()
+        # position = self._path_list.index(current_song_path)
+        # goto_index = self._path_list.index(path)
+        # song_offset = goto_index - position
+        # self._player.play(song_offset=song_offset)
 
     def _track_list_replaced(self, tracks, current_song):
         parameters = {
@@ -718,7 +762,7 @@ class MPRIS(DBusInterface):
                 break
 
         if selected_playlist is not None:
-            self._playlists.activate_playlist(selected_playlist)
+            self._coremodel.activate_playlist(selected_playlist)
 
     def _get_playlists(self, index, max_count, order, reverse):
         """Gets a set of playlists (MPRIS Method).
diff --git a/gnomemusic/player.py b/gnomemusic/player.py
index 4dc6f49a..febc1d95 100644
--- a/gnomemusic/player.py
+++ b/gnomemusic/player.py
@@ -22,29 +22,24 @@
 # 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 defaultdict
 from enum import IntEnum
-from itertools import chain
-from random import shuffle, randrange
+from random import randint, randrange
 import logging
 import time
 
 import gi
-gi.require_version('Grl', '0.3')
-gi.require_version('Gst', '1.0')
-gi.require_version('GstAudio', '1.0')
 gi.require_version('GstPbutils', '1.0')
-from gi.repository import GObject, Grl, GstPbutils
+from gi.repository import GObject, GstPbutils
+from gi._gi import pygobject_new_full
 
 from gnomemusic import log
+from gnomemusic.coresong import CoreSong
 from gnomemusic.gstplayer import GstPlayer, Playback
-from gnomemusic.grilo import grilo
-from gnomemusic.playlists import Playlists
 from gnomemusic.scrobbler import LastFmScrobbler
+from gnomemusic.widgets.songwidget import SongWidget
 
 
 logger = logging.getLogger(__name__)
-playlists = Playlists.get_default()
 
 
 class RepeatMode(IntEnum):
@@ -55,14 +50,6 @@ class RepeatMode(IntEnum):
     SHUFFLE = 3
 
 
-class ValidationStatus(IntEnum):
-    """Enum for song validation"""
-    PENDING = 0
-    IN_PROGRESS = 1
-    FAILED = 2
-    SUCCEEDED = 3
-
-
 class PlayerField(IntEnum):
     """Enum for player model fields"""
     SONG = 0
@@ -84,122 +71,31 @@ class PlayerPlaylist(GObject.GObject):
         PLAYLIST = 3
         SEARCH_RESULT = 4
 
-    __gsignals__ = {
-        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
-    }
-
-    _nb_songs_max = 10
-
     repeat_mode = GObject.Property(type=int, default=RepeatMode.NONE)
 
     def __repr__(self):
         return '<PlayerPlayList>'
 
     @log
-    def __init__(self):
+    def __init__(self, application):
         super().__init__()
 
         GstPbutils.pb_utils_init()
 
-        self._songs = []
-        self._shuffle_indexes = []
-        self._current_index = 0
+        self._app = application
+        self._position = 0
 
         self._type = -1
         self._id = -1
 
-        self._validation_indexes = None
+        self._validation_songs = {}
         self._discoverer = GstPbutils.Discoverer()
-        self._discoverer.connect('discovered', self._on_discovered)
+        self._discoverer.connect("discovered", self._on_discovered)
         self._discoverer.start()
 
-        self.connect("notify::repeat-mode", self._on_repeat_mode_changed)
+        self._model = self._app.props.coremodel.props.playlist_sort
 
-    @log
-    def set_playlist(self, playlist_type, playlist_id, model, model_iter=None):
-        """Set a new playlist or change the song being played
-
-        If no song is requested (through model_iter), a song will be
-        automatically selected:
-        * the first song in a linear mode
-        * a random song in shuffle mode
-
-        :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
-
-        :return: True if the playlist has been updated. False otherwise
-        :rtype: bool
-        """
-        if not model_iter:
-            if self.props.repeat_mode == RepeatMode.SHUFFLE:
-                index = randrange(len(model))
-                model_iter = model.get_iter_from_string(str(index))
-            else:
-                model_iter = model.get_iter_first()
-
-        path = model.get_path(model_iter)
-        self._current_index = int(path.to_string())
-
-        # 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
-
-        self._validation_indexes = defaultdict(list)
-        self._type = playlist_type
-        self._id = playlist_id
-
-        self._songs = []
-        for row in model:
-            self._songs.append([row[5], row[11]])
-
-        if self.props.repeat_mode == 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 set_song(self, song_offset):
-        """Change playlist index.
-
-        :param int song_offset: position relative to current song
-        :return: True if the index has changed. False otherwise.
-        :rtype: bool
-        """
-        if self.props.repeat_mode == RepeatMode.SHUFFLE:
-            shuffle = self._shuffle_indexes.index(self._current_index)
-            self._current_index = self._shuffle_indexes[shuffle + song_offset]
-            return True
-
-        song_index = song_offset + self._current_index
-        if self.props.repeat_mode == RepeatMode.ALL:
-            song_index = song_index % len(self._songs)
-
-        if(song_index >= len(self._songs)
-           or song_index < 0):
-            return False
-
-        self._current_index = song_index
-        return True
+        self.connect("notify::repeat-mode", self._on_repeat_mode_changed)
 
     @log
     def change_position(self, prev_pos, new_pos):
@@ -210,33 +106,7 @@ class PlayerPlaylist(GObject.GObject):
         :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.props.repeat_mode == 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
+        pass
 
     @log
     def add_song(self, song, song_index):
@@ -245,19 +115,7 @@ class PlayerPlaylist(GObject.GObject):
         :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.props.repeat_mode == 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)
+        pass
 
     @log
     def remove_song(self, song_index):
@@ -265,102 +123,7 @@ class PlayerPlaylist(GObject.GObject):
 
         :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.props.repeat_mode == 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_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 is being processed or 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 validation of {} as not a local file".format(url))
-            return
-
-        item[PlayerField.VALIDATION] = ValidationStatus.IN_PROGRESS
-        self._validation_indexes[url].append(index)
-        self._discoverer.discover_uri_async(url)
-
-    @log
-    def _get_next_index(self):
-        if not self.has_next():
-            return -1
-
-        if self.props.repeat_mode == RepeatMode.SONG:
-            return self._current_index
-        if (self.props.repeat_mode == RepeatMode.ALL
-                and self._current_index == (len(self._songs) - 1)):
-            return 0
-        if self.props.repeat_mode == RepeatMode.SHUFFLE:
-            index = self._shuffle_indexes.index(self._current_index)
-            return self._shuffle_indexes[index + 1]
-        else:
-            return self._current_index + 1
-
-    @log
-    def _get_previous_index(self):
-        if not self.has_previous():
-            return -1
-
-        if self.props.repeat_mode == RepeatMode.SONG:
-            return self._current_index
-        if (self.props.repeat_mode == RepeatMode.ALL
-                and self._current_index == 0):
-            return len(self._songs) - 1
-        if self.props.repeat_mode == RepeatMode.SHUFFLE:
-            shuffle_index = self._shuffle_indexes.index(self._current_index)
-            return self._shuffle_indexes[shuffle_index - 1]
-        else:
-            return self._current_index - 1
-
-    @log
-    def _validate_next_song(self):
-        if self.props.repeat_mode == RepeatMode.SONG:
-            return
-
-        next_index = self._get_next_index()
-        if next_index >= 0:
-            self._validate_song(next_index)
-
-    @log
-    def _validate_previous_song(self):
-        if self.props.repeat_mode == RepeatMode.SONG:
-            return
-
-        previous_index = self._get_previous_index()
-        if previous_index >= 0:
-            self._validate_song(previous_index)
+        pass
 
     @log
     def has_next(self):
@@ -369,13 +132,12 @@ class PlayerPlaylist(GObject.GObject):
         :return: True if there is a song. False otherwise.
         :rtype: bool
         """
-        if (self.props.repeat_mode == RepeatMode.SHUFFLE
-                and self._shuffle_indexes):
-            index = self._shuffle_indexes.index(self._current_index)
-            return index < (len(self._shuffle_indexes) - 1)
-        if self.props.repeat_mode != RepeatMode.NONE:
+        if (self.props.repeat_mode == RepeatMode.SONG
+                or self.props.repeat_mode == RepeatMode.ALL
+                or self.props.position < self._model.get_n_items() - 1):
             return True
-        return self._current_index < (len(self._songs) - 1)
+
+        return False
 
     @log
     def has_previous(self):
@@ -384,13 +146,13 @@ class PlayerPlaylist(GObject.GObject):
         :return: True if there is a song. False otherwise.
         :rtype: bool
         """
-        if (self.props.repeat_mode == RepeatMode.SHUFFLE
-                and self._shuffle_indexes):
-            index = self._shuffle_indexes.index(self._current_index)
-            return index > 0
-        if self.props.repeat_mode != RepeatMode.NONE:
+        if (self.props.repeat_mode == RepeatMode.SONG
+                or self.props.repeat_mode == RepeatMode.ALL
+                or (self.props.position <= self._model.get_n_items() - 1
+                    and self.props.position > 0)):
             return True
-        return self._current_index > 0
+
+        return False
 
     @log
     def next(self):
@@ -399,15 +161,27 @@ class PlayerPlaylist(GObject.GObject):
         :return: True if the operation succeeded. False otherwise.
         :rtype: bool
         """
-        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:
-                return self.next()
-        return False
+        if not self.has_next():
+            return False
+
+        if self.props.repeat_mode == RepeatMode.SONG:
+            next_position = self.props.position
+        elif (self.props.repeat_mode == RepeatMode.ALL
+                and self.props.position == self._model.get_n_items() - 1):
+            next_position = 0
+        else:
+            next_position = self.props.position + 1
+
+        self._model[self.props.position].props.state = SongWidget.State.PLAYED
+        self._position = next_position
+
+        next_song = self._model[next_position]
+        if next_song.props.validation == CoreSong.Validation.FAILED:
+            return self.next()
+
+        next_song.props.state = SongWidget.State.PLAYING
+        self._validate_next_song()
+        return True
 
     @log
     def previous(self):
@@ -416,54 +190,167 @@ class PlayerPlaylist(GObject.GObject):
         :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:
-                return self.previous()
-        return False
+        if not self.has_previous():
+            return False
+
+        if self.props.repeat_mode == RepeatMode.SONG:
+            previous_position = self.props.position
+        elif (self.props.repeat_mode == RepeatMode.ALL
+                and self.props.position == 0):
+            previous_position = self._model.get_n_items() - 1
+        else:
+            previous_position = self.props.position - 1
+
+        self._model[self.props.position].props.state = SongWidget.State.PLAYED
+        self._position = previous_position
+
+        previous_song = self._model[previous_position]
+        if previous_song.props.validation == CoreSong.Validation.FAILED:
+            return self.previous()
+
+        self._model[previous_position].props.state = SongWidget.State.PLAYING
+        self._validate_previous_song()
+        return True
 
     @GObject.Property(type=int, default=0, flags=GObject.ParamFlags.READABLE)
-    def current_song_index(self):
+    def position(self):
         """Gets current song index.
 
         :returns: position of the current song in the playlist.
         :rtype: int
         """
-        return self._current_index
+        return self._position
 
     @GObject.Property(
-        type=Grl.Media, default=None, flags=GObject.ParamFlags.READABLE)
+        type=CoreSong, default=None, flags=GObject.ParamFlags.READABLE)
     def current_song(self):
         """Get current song.
 
         :returns: the song being played or None if there are no songs
-        :rtype: Grl.Media
+        :rtype: CoreSong
         """
-        if self._songs:
-            return self._songs[self._current_index][PlayerField.SONG]
+        n_items = self._model.get_n_items()
+        if (n_items != 0
+                and n_items > self._position):
+            current_song = self._model[self._position]
+            if current_song.props.state == SongWidget.State.PLAYING:
+                return current_song
+
+        for idx, coresong in enumerate(self._model):
+            if coresong.props.state == SongWidget.State.PLAYING:
+                print("position", idx)
+                self._position = idx
+                return coresong
+
         return None
 
-    def _current_song_is_valid(self):
-        """Check if current song can be played.
+    def set_song(self, song):
+        """Sets current song.
 
-        :returns: False if validation failed
-        :rtype: bool
+        If no song is provided, a song is automatically selected.
+
+        :param CoreSong song: song to set
+        :returns: The selected song
+        :rtype: CoreSong
         """
-        current_item = self._songs[self._current_index]
-        return current_item[PlayerField.VALIDATION] != ValidationStatus.FAILED
+        if song is None:
+            if self.props.repeat_mode == RepeatMode.SHUFFLE:
+                position = randrange(0, self._model.get_n_items())
+            else:
+                position = 0
+            song = self._model.get_item(position)
+            song.props.state = SongWidget.State.PLAYING
+            self._position = position
+            self._validate_song(song)
+            self._validate_next_song()
+            return song
+
+        for idx, coresong in enumerate(self._model):
+            if coresong == song:
+                coresong.props.state = SongWidget.State.PLAYING
+                self._position = idx
+                self._validate_song(song)
+                self._validate_next_song()
+                return song
+
+        return None
 
     @log
     def _on_repeat_mode_changed(self, klass, param):
-        if (self.props.repeat_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)
+
+        def _wrap_list_store_sort_func(func):
+            def wrap(a, b, *user_data):
+                a = pygobject_new_full(a, False)
+                b = pygobject_new_full(b, False)
+                return func(a, b, *user_data)
+
+            return wrap
+
+        # FIXME: This shuffle is too simple.
+        def _shuffle_sort(song_a, song_b):
+            return randint(-1, 1)
+
+        if self.props.repeat_mode == RepeatMode.SHUFFLE:
+            self._model.set_sort_func(
+                _wrap_list_store_sort_func(_shuffle_sort))
+        elif self.props.repeat_mode in [RepeatMode.NONE, RepeatMode.ALL]:
+            self._model.set_sort_func(None)
+
+    def _validate_song(self, coresong):
+        # Song is being processed or has already been processed.
+        # Nothing to do.
+        if coresong.props.validation > CoreSong.Validation.PENDING:
+            return
+
+        url = coresong.props.url
+        if not url:
+            logger.warning(
+                "The item {} doesn't have a URL set.".format(coresong))
+            return
+        if not url.startswith("file://"):
+            logger.debug(
+                "Skipping validation of {} as not a local file".format(url))
+            return
+
+        coresong.props.validation = CoreSong.Validation.IN_PROGRESS
+        self._validation_songs[url] = coresong
+        self._discoverer.discover_uri_async(url)
+
+    def _validate_next_song(self):
+        if self.props.repeat_mode == RepeatMode.SONG:
+            return
+
+        current_position = self.props.position
+        next_position = current_position + 1
+        if next_position == self._model.get_n_items():
+            if self.props.repeat_mode != RepeatMode.ALL:
+                return
+            next_position = 0
+
+        self._validate_song(self._model[next_position])
+
+    def _validate_previous_song(self):
+        if self.props.repeat_mode == RepeatMode.SONG:
+            return
+
+        current_position = self.props.position
+        previous_position = current_position - 1
+        if previous_position < 0:
+            if self.props.repeat_mode != RepeatMode.ALL:
+                return
+            previous_position = self._model.get_n_items() - 1
+
+        self._validate_song(self._model[previous_position])
+
+    def _on_discovered(self, discoverer, info, error):
+        url = info.get_uri()
+        coresong = self._validation_songs[url]
+
+        if error:
+            logger.warning("Info {}: error: {}".format(info, error))
+            coresong.props.validation = CoreSong.Validation.FAILED
+        else:
+            coresong.props.validation = CoreSong.Validation.SUCCEEDED
 
     @GObject.Property(type=int, flags=GObject.ParamFlags.READABLE)
     def playlist_id(self):
@@ -483,57 +370,6 @@ class PlayerPlaylist(GObject.GObject):
         """
         return self._type
 
-    @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
-        """
-        if not self.props.current_song:
-            return []
-
-        songs = []
-        nb_songs = len(self._songs)
-        current_index = self._current_index
-        if self.props.repeat_mode == RepeatMode.SHUFFLE:
-            current_index = self._shuffle_indexes.index(self._current_index)
-
-        index_min = current_index - self._nb_songs_max
-        index_max = current_index + self._nb_songs_max + 1
-        if self._type == PlayerPlaylist.Type.ALBUM:
-            index_min = 0
-            index_max = nb_songs
-
-        first_index = max(index_min, 0)
-        last_index = min(index_max, nb_songs)
-
-        if self.props.repeat_mode == RepeatMode.SHUFFLE:
-            indexes = self._shuffle_indexes[first_index:last_index]
-        else:
-            indexes = range(first_index, last_index)
-
-        if (self.props.repeat_mode == RepeatMode.ALL
-                and (last_index - first_index) < (2 * self._nb_songs_max + 1)):
-            offset_sup = min(
-                self._nb_songs_max - last_index + current_index + 1,
-                first_index)
-            offset_inf = min(
-                self._nb_songs_max - current_index + first_index,
-                nb_songs - last_index)
-
-            indexes = chain(
-                range(nb_songs - offset_inf, nb_songs), indexes,
-                range(offset_sup))
-
-        songs = [[index, self._songs[index][PlayerField.SONG]]
-                 for index in indexes]
-        return songs
-
 
 class Player(GObject.GObject):
     """Main Player object
@@ -544,8 +380,7 @@ class Player(GObject.GObject):
     __gsignals__ = {
         'playlist-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
         'seek-finished': (GObject.SignalFlags.RUN_FIRST, None, ()),
-        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
-        'song-validated': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
+        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ())
     }
 
     state = GObject.Property(type=int, default=Playback.STOPPED)
@@ -562,6 +397,7 @@ class Player(GObject.GObject):
         """
         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
@@ -570,8 +406,7 @@ class Player(GObject.GObject):
         # needed.
         self._gapless_set = False
 
-        self._playlist = PlayerPlaylist()
-        self._playlist.connect('song-validated', self._on_song_validated)
+        self._playlist = PlayerPlaylist(self._app)
 
         self._settings = application.props.settings
         self._settings.connect(
@@ -588,6 +423,7 @@ class Player(GObject.GObject):
         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(
@@ -627,19 +463,12 @@ class Player(GObject.GObject):
         """
         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()
-
     @log
     def _on_about_to_finish(self, klass):
         if self.props.has_next:
             self._playlist.next()
 
-            new_url = self._playlist.props.current_song.get_url()
+            new_url = self._playlist.props.current_song.props.url
             self._gst_player.props.url = new_url
             self._gapless_set = True
 
@@ -655,32 +484,47 @@ class Player(GObject.GObject):
 
         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, song_changed=True, song_offset=None):
+    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
-        :param int song_offset: position relative to current song
         """
         if self.props.current_song is None:
-            return
+            coresong = self._playlist.set_song(coresong)
 
-        if (song_offset is not None
-                and not self._playlist.set_song(song_offset)):
-            return False
+        if (coresong is not None
+                and coresong.props.validation == CoreSong.Validation.FAILED
+                and self.props.repeat_mode != RepeatMode.SONG):
+            self._on_error()
+            return
 
-        if (song_changed
-                or self._gapless_set):
-            self._load(self._playlist.props.current_song)
+        if coresong is not None:
+            self._load(coresong)
 
         self._gst_player.props.state = Playback.PLAYING
 
@@ -701,7 +545,7 @@ class Player(GObject.GObject):
         Play the next song of the playlist, if any.
         """
         if self._playlist.next():
-            self.play()
+            self.play(self._playlist.props.current_song)
 
     @log
     def previous(self):
@@ -715,7 +559,7 @@ class Player(GObject.GObject):
             return
 
         if self._playlist.previous():
-            self.play()
+            self.play(self._playlist.props.current_song)
 
     @log
     def play_pause(self):
@@ -723,22 +567,7 @@ class Player(GObject.GObject):
         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')
+            self.play()
 
     @log
     def playlist_change_position(self, prev_pos, new_pos):
@@ -760,7 +589,7 @@ class Player(GObject.GObject):
 
         :param int song_index: position of the song to remove
         """
-        if self.props.current_song_index == song_index:
+        if self.props.position == song_index:
             if self.props.has_next:
                 self.next()
             elif self.props.has_previous:
@@ -779,11 +608,6 @@ class Player(GObject.GObject):
         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.
@@ -826,9 +650,10 @@ class Player(GObject.GObject):
                 # 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)
+                # 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):
@@ -847,21 +672,21 @@ class Player(GObject.GObject):
         self._settings.set_enum('repeat', mode)
 
     @GObject.Property(type=int, default=0, flags=GObject.ParamFlags.READABLE)
-    def current_song_index(self):
+    def position(self):
         """Gets current song index.
 
         :returns: position of the current song in the playlist.
         :rtype: int
         """
-        return self._playlist.props.current_song_index
+        return self._playlist.props.position
 
     @GObject.Property(
-        type=Grl.Media, default=None, flags=GObject.ParamFlags.READABLE)
+        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: Grl.Media
+        :returns: The song being played. None if there is no playlist.
+        :rtype: CoreSong
         """
         return self._playlist.props.current_song
 
@@ -909,19 +734,6 @@ class Player(GObject.GObject):
         if position_second <= duration_second:
             self._gst_player.seek(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()
-
     @log
     def _on_seek_finished(self, klass):
         # FIXME: Just a proxy
diff --git a/gnomemusic/scrobbler.py b/gnomemusic/scrobbler.py
index b6ab4f64..e44fccd9 100644
--- a/gnomemusic/scrobbler.py
+++ b/gnomemusic/scrobbler.py
@@ -211,12 +211,12 @@ class LastFmScrobbler(GObject.GObject):
             logger.warning(msg.props.response_body.data)
 
     @log
-    def scrobble(self, media, time_stamp):
+    def scrobble(self, coresong, time_stamp):
         """Scrobble a song to Last.fm.
 
         If not connected to Last.fm nothing happens
 
-        :param media: Grilo media item
+        :param coresong: CoreSong to scrobble
         :param time_stamp: song loaded time (epoch time)
         """
         self.scrobbled = True
@@ -224,19 +224,21 @@ class LastFmScrobbler(GObject.GObject):
         if self._goa_lastfm.disabled:
             return
 
+        media = coresong.props.media
         self._lastfm_api_call(media, time_stamp, "scrobble")
 
     @log
-    def now_playing(self, media):
+    def now_playing(self, coresong):
         """Set now playing song to Last.fm
 
         If not connected to Last.fm nothing happens
 
-        :param media: Grilo media item
+        :param coresong: CoreSong to use for now playing
         """
         self.scrobbled = False
 
         if self._goa_lastfm.disabled:
             return
 
+        media = coresong.props.media
         self._lastfm_api_call(media, None, "update now playing")
diff --git a/gnomemusic/songliststore.py b/gnomemusic/songliststore.py
new file mode 100644
index 00000000..b923943e
--- /dev/null
+++ b/gnomemusic/songliststore.py
@@ -0,0 +1,91 @@
+from gi.repository import Gfm, Gio, GObject, Gtk
+from gi._gi import pygobject_new_full
+
+import gnomemusic.utils as utils
+
+
+class SongListStore(Gtk.ListStore):
+
+    def __init__(self, model):
+        super().__init__()
+
+        self._model = Gfm.SortListModel.new(model)
+        self._model.set_sort_func(
+            self._wrap_list_store_sort_func(self._songs_sort))
+
+        self.set_column_types([
+            GObject.TYPE_STRING,    # play or invalid icon
+            GObject.TYPE_BOOLEAN,   # selected
+            GObject.TYPE_STRING,    # title
+            GObject.TYPE_STRING,    # artist
+            GObject.TYPE_STRING,    # album
+            GObject.TYPE_STRING,    # duration
+            GObject.TYPE_INT,       # favorite
+            GObject.TYPE_OBJECT,    # coresong
+            GObject.TYPE_INT,       # validation
+            GObject.TYPE_BOOLEAN,   # iter_to_clean
+        ])
+
+        self._model.connect("items-changed", self._on_items_changed)
+
+    def _wrap_list_store_sort_func(self, func):
+
+        def wrap(a, b, *user_data):
+            a = pygobject_new_full(a, False)
+            b = pygobject_new_full(b, False)
+            return func(a, b, *user_data)
+
+        return wrap
+
+    def _songs_sort(self, song_a, song_b):
+        title_a = song_a.props.title.casefold()
+        title_b = song_b.props.title.casefold()
+        song_cmp = title_a == title_b
+        if not song_cmp:
+            return title_a > title_b
+
+        artist_a = song_a.props.artist.casefold()
+        artist_b = song_b.props.artist.casefold()
+        artist_cmp = artist_a == artist_b
+        if not artist_cmp:
+            return artist_a > artist_b
+
+        return song_a.props.album.casefold() > song_b.props.album.casefold()
+
+    def _on_items_changed(self, model, position, removed, added):
+        if removed > 0:
+            for i in list(range(removed)):
+                path = Gtk.TreePath.new_from_string("{}".format(position))
+                iter_ = self.get_iter(path)
+                self.remove(iter_)
+
+        if added > 0:
+            for i in list(range(added)):
+                coresong = model[position]
+                time = utils.seconds_to_string(coresong.props.duration)
+                self.insert_with_valuesv(
+                    position, [2, 3, 4, 5, 6, 7],
+                    [coresong.props.title, coresong.props.artist,
+                     coresong.props.album, time,
+                     int(coresong.props.favorite), coresong])
+                coresong.connect(
+                    "notify::favorite", self._on_favorite_changed)
+                coresong.connect(
+                    "notify::state", self._on_state_changed)
+
+    def _on_favorite_changed(self, coresong, value):
+        for row in self:
+            if coresong == row[7]:
+                row[6] = coresong.props.favorite
+                break
+
+    def _on_state_changed(self, coresong, value):
+        for row in self:
+            if coresong == row[7]:
+                row[8] = coresong.props.validation
+                break
+
+    @GObject.Property(
+        type=Gio.ListStore, default=None, flags=GObject.ParamFlags.READABLE)
+    def model(self):
+        return self._model
diff --git a/gnomemusic/utils.py b/gnomemusic/utils.py
index bae746e7..bd5d67d1 100644
--- a/gnomemusic/utils.py
+++ b/gnomemusic/utils.py
@@ -89,6 +89,9 @@ def get_media_title(item):
 
     if not title:
         url = item.get_url()
+        # FIXME
+        if url is None:
+            return "NO URL"
         file_ = Gio.File.new_for_uri(url)
         fileinfo = file_.query_info(
             "standard::display-name", Gio.FileQueryInfoFlags.NONE, None)
@@ -102,13 +105,13 @@ def get_media_year(item):
     """Returns the year when the media was created.
 
     :param item: A Grilo Media object
-    :return: The creation year or None if not defined
+    :return: The creation year or '----' if not defined
     :rtype: string
     """
     date = item.get_creation_date()
 
     if not date:
-        return None
+        return "----"
 
     return str(date.get_year())
 
diff --git a/gnomemusic/views/albumsview.py b/gnomemusic/views/albumsview.py
index 76af3cfe..b2b1e90e 100644
--- a/gnomemusic/views/albumsview.py
+++ b/gnomemusic/views/albumsview.py
@@ -26,12 +26,10 @@ from gettext import gettext as _
 from gi.repository import GObject, Gtk
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
 from gnomemusic.views.baseview import BaseView
 from gnomemusic.widgets.headerbar import HeaderBar
 from gnomemusic.widgets.albumcover import AlbumCover
 from gnomemusic.widgets.albumwidget import AlbumWidget
-import gnomemusic.utils as utils
 
 
 class AlbumsView(BaseView):
@@ -43,16 +41,14 @@ class AlbumsView(BaseView):
 
     @log
     def __init__(self, window, player):
+        self._window = window
         super().__init__('albums', _("Albums"), window)
 
         self.player = player
-
-        self._album_widget = AlbumWidget(player)
+        self._album_widget = AlbumWidget(player, self)
         self._album_widget.bind_property(
             "selection-mode", self, "selection-mode",
             GObject.BindingFlags.BIDIRECTIONAL)
-        self._album_widget.bind_property(
-            "selected-items-count", self, "selected-items-count")
 
         self.add(self._album_widget)
         self.albums_selected = []
@@ -63,19 +59,11 @@ class AlbumsView(BaseView):
         self.connect(
             "notify::search-mode-active", self._on_search_mode_changed)
 
-    @log
-    def _on_changes_pending(self, data=None):
-        if (self._init and not self.props.selection_mode):
-            self._offset = 0
-            self._populate()
-            grilo.changes_pending['Albums'] = False
-
     @log
     def _on_selection_mode_changed(self, widget, data=None):
         super()._on_selection_mode_changed(widget, data)
 
-        if (not self.props.selection_mode
-                and grilo.changes_pending['Albums']):
+        if not self.props.selection_mode:
             self._on_changes_pending()
 
     @log
@@ -91,7 +79,7 @@ class AlbumsView(BaseView):
             homogeneous=True, hexpand=True, halign=Gtk.Align.FILL,
             valign=Gtk.Align.START, selection_mode=Gtk.SelectionMode.NONE,
             margin=18, row_spacing=12, column_spacing=6,
-            min_children_per_line=1, max_children_per_line=20)
+            min_children_per_line=1, max_children_per_line=20, visible=True)
 
         self._view.get_style_context().add_class('content-view')
         self._view.connect('child-activated', self._on_child_activated)
@@ -102,6 +90,29 @@ class AlbumsView(BaseView):
 
         self._box.add(scrolledwin)
 
+        self._model = self._window._app.props.coremodel.props.albums_sort
+        self._view.bind_model(self._model, self._create_widget)
+
+        self._view.show()
+
+    @log
+    def _create_widget(self, corealbum):
+        album_widget = AlbumCover(corealbum)
+
+        self.bind_property(
+            "selection-mode", album_widget, "selection-mode",
+            GObject.BindingFlags.SYNC_CREATE
+            | GObject.BindingFlags.BIDIRECTIONAL)
+
+        # NOTE: Adding SYNC_CREATE here will trigger all the nested
+        # models to be created. This will slow down initial start,
+        # but will improve initial 'selecte all' speed.
+        album_widget.bind_property(
+            "selected", corealbum, "selected",
+            GObject.BindingFlags.BIDIRECTIONAL)
+
+        return album_widget
+
     @log
     def _back_button_clicked(self, widget, data=None):
         self._headerbar.state = HeaderBar.State.MAIN
@@ -109,109 +120,40 @@ class AlbumsView(BaseView):
 
     @log
     def _on_child_activated(self, widget, child, user_data=None):
+        corealbum = child.props.corealbum
         if self.props.selection_mode:
             return
 
-        item = child.props.media
         # Update and display the album widget if not in selection mode
-        self._album_widget.update(item)
+        self._album_widget.update(corealbum)
 
-        self._set_album_headerbar(item)
+        self._set_album_headerbar(corealbum)
         self.set_visible_child(self._album_widget)
 
     @log
-    def _set_album_headerbar(self, album):
+    def _set_album_headerbar(self, corealbum):
         self._headerbar.props.state = HeaderBar.State.CHILD
-        self._headerbar.props.title = utils.get_album_title(album)
-        self._headerbar.props.subtitle = utils.get_artist_name(album)
+        self._headerbar.props.title = corealbum.props.title
+        self._headerbar.props.subtitle = corealbum.props.artist
 
     @log
     def _populate(self, data=None):
-        self._window.notifications_popup.push_loading()
-        grilo.populate_albums(self._offset, self._add_item)
+        # self._window.notifications_popup.push_loading()
         self._init = True
-
-    @log
-    def get_selected_songs(self, callback):
-        # FIXME: we call into private objects with full knowledge of
-        # what is there
-        if self._headerbar.props.state == HeaderBar.State.CHILD:
-            callback(self._album_widget._disc_listbox.get_selected_items())
-        else:
-            self.items_selected = []
-            self.items_selected_callback = callback
-            self.albums_index = 0
-            if len(self.albums_selected):
-                self._get_selected_album_songs()
-
-    @log
-    def _add_item(self, source, param, item, remaining=0, data=None):
-        if item:
-            # Store all items to optimize 'Select All' action
-            self.all_items.append(item)
-
-            # Add to the flowbox
-            child = self._create_album_item(item)
-            self._view.add(child)
-            self._offset += 1
-        elif remaining == 0:
-            self._view.show()
-            self._window.notifications_popup.pop_loading()
-            self._init = False
-
-    def _create_album_item(self, item):
-        child = AlbumCover(item)
-
-        child.connect('notify::selected', self._on_selection_changed)
-
-        self.bind_property(
-            'selection-mode', child, 'selection-mode',
-            GObject.BindingFlags.BIDIRECTIONAL)
-
-        return child
-
-    @log
-    def _on_selection_changed(self, child, data=None):
-        if (child.props.selected
-                and child.props.media not in self.albums_selected):
-            self.albums_selected.append(child.props.media)
-        elif (not child.props.selected
-                and child.props.media in self.albums_selected):
-            self.albums_selected.remove(child.props.media)
-
-        self.props.selected_items_count = len(self.albums_selected)
-
-    @log
-    def _get_selected_album_songs(self):
-        grilo.populate_album_songs(
-            self.albums_selected[self.albums_index],
-            self._add_selected_item)
-        self.albums_index += 1
-
-    @log
-    def _add_selected_item(self, source, param, item, remaining=0, data=None):
-        if item:
-            self.items_selected.append(item)
-        if remaining == 0:
-            if self.albums_index < self.props.selected_items_count:
-                self._get_selected_album_songs()
-            else:
-                self.items_selected_callback(self.items_selected)
+        self._view.show()
 
     def _toggle_all_selection(self, selected):
         """
         Selects or unselects all items without sending the notify::active
         signal for performance purposes.
         """
-        for child in self._view.get_children():
-            child.props.selected = selected
+        with self._window._app.props.coreselection.freeze_notify():
+            for child in self._view.get_children():
+                child.props.selected = selected
+                child.props.corealbum.props.selected = selected
 
-    @log
     def select_all(self):
-        self.albums_selected = list(self.all_items)
         self._toggle_all_selection(True)
 
-    @log
     def unselect_all(self):
-        self.albums_selected = []
         self._toggle_all_selection(False)
diff --git a/gnomemusic/views/artistsview.py b/gnomemusic/views/artistsview.py
index 1eb93a01..7369924d 100644
--- a/gnomemusic/views/artistsview.py
+++ b/gnomemusic/views/artistsview.py
@@ -27,12 +27,9 @@ from gettext import gettext as _
 from gi.repository import Gdk, 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
-import gnomemusic.utils as utils
+from gnomemusic.widgets.artisttile import ArtistTile
 
 logger = logging.getLogger(__name__)
 
@@ -63,6 +60,16 @@ class ArtistsView(BaseView):
         self.player = player
         self._artists = {}
 
+        self._window = window
+        self._coremodel = window._app.props.coremodel
+        self._model = self._coremodel.props.artists_sort
+
+        self._model.connect_after(
+            "items-changed", self._on_model_items_changed)
+        self._sidebar.bind_model(self._model, self._create_widget)
+        self._loaded_id = self._coremodel.connect(
+            "artists-loaded", self._on_artists_loaded)
+
         sidebar_container.props.width_request = 220
         sidebar_container.get_style_context().add_class('sidebar')
         self._sidebar.props.selection_mode = Gtk.SelectionMode.SINGLE
@@ -73,22 +80,63 @@ class ArtistsView(BaseView):
         self._ctrl.props.button = Gdk.BUTTON_PRIMARY
         self._ctrl.connect("released", self._on_sidebar_clicked)
 
+        self._loaded_artists = []
+        self._loading_id = 0
+
         self.show_all()
-        self._sidebar.hide()
+
+    def _create_widget(self, coreartist):
+        row = ArtistTile(coreartist)
+        row.props.text = coreartist.props.artist
+
+        self.bind_property("selection-mode", row, "selection-mode")
+
+        return row
+
+    def _on_model_items_changed(self, model, position, removed, added):
+        if removed == 0:
+            return
+
+        removed_artist = None
+        artists = [coreartist.props.artist for coreartist in model]
+        for artist in self._loaded_artists:
+            if artist not in artists:
+                removed_artist = artist
+                break
+
+        if removed_artist is None:
+            return
+
+        self._loaded_artists.remove(removed_artist)
+        if self._view.get_visible_child_name() == removed_artist:
+            row_next = (self._sidebar.get_row_at_index(position)
+                        or self._sidebar.get_row_at_index(position - 1))
+            if row_next:
+                self._sidebar.select_row(row_next)
+                row_next.emit("activate")
+
+        removed_frame = self._view.get_child_by_name(removed_artist)
+        self._view.remove(removed_frame)
+
+    def _on_artists_loaded(self, klass):
+        self._coremodel.disconnect(self._loaded_id)
+        first_row = self._sidebar.get_row_at_index(0)
+        self._sidebar.select_row(first_row)
+        first_row.emit("activate")
 
     @log
     def _setup_view(self):
-        view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
-        self._box.pack_start(view_container, True, True, 0)
+        self._view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
+        self._box.pack_start(self._view_container, True, True, 0)
 
         self._view = Gtk.Stack(
-            transition_type=Gtk.StackTransitionType.CROSSFADE)
-        view_container.add(self._view)
+            transition_type=Gtk.StackTransitionType.CROSSFADE,
+            vhomogeneous=False)
+        self._view_container.add(self._view)
 
-        self._artist_albums_widget = Gtk.Frame(
-            shadow_type=Gtk.ShadowType.NONE, hexpand=True)
-        self._view.add_named(self._artist_albums_widget, "artist-albums")
-        self._view.props.visible_child_name = "artist-albums"
+        empty_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE, hexpand=True)
+        empty_frame.show()
+        self._view.add_named(empty_frame, "empty-frame")
 
     @log
     def _on_changes_pending(self, data=None):
@@ -97,81 +145,57 @@ class ArtistsView(BaseView):
             self._artists.clear()
             self._offset = 0
             self._populate()
-            grilo.changes_pending['Artists'] = False
 
     @log
     def _on_artist_activated(self, sidebar, row, data=None):
         """Initializes new artist album widgets"""
+        artist_tile = row.get_child()
         if self.props.selection_mode:
-            row.props.selected = not row.props.selected
+            artist_tile.props.selected = not artist_tile.props.selected
             return
 
-        self._last_selected_row = row
-        artist = row.props.text
-        albums = self._artists[artist.casefold()]['albums']
-        widget = self._artists[artist.casefold()]['widget']
-
-        if widget:
-            if self.player.playing_playlist(
-                    PlayerPlaylist.Type.ARTIST, widget.artist):
-                self._artist_albums_widget = widget.get_parent()
-                self._view.set_visible_child(self._artist_albums_widget)
-                return
-            elif widget.get_parent() == self._view:
-                return
-            else:
-                widget.get_parent().destroy()
-
         # Prepare a new artist_albums_widget here
-        new_artist_albums_widget = Gtk.Frame(
-            shadow_type=Gtk.ShadowType.NONE, hexpand=True)
-        self._view.add(new_artist_albums_widget)
+        coreartist = artist_tile.props.coreartist
+        if coreartist.props.artist in self._loaded_artists:
+            scroll_vadjustment = self._view_container.props.vadjustment
+            scroll_vadjustment.props.value = 0.
+            self._view.set_visible_child_name(coreartist.props.artist)
+            return
 
-        artist_albums = ArtistAlbumsWidget(
-            artist, albums, self.player, self._window)
-        self._artists[artist.casefold()]['widget'] = artist_albums
-        new_artist_albums_widget.add(artist_albums)
-        new_artist_albums_widget.show()
+        if self._loading_id > 0:
+            self._artist_albums.disconnect(self._loading_id)
+            self._loading_id = 0
 
-        # Replace previous widget
-        self._artist_albums_widget = new_artist_albums_widget
-        self._view.set_visible_child(new_artist_albums_widget)
+        self._artist_albums = ArtistAlbumsWidget(
+            coreartist, self.player, self._window, False)
+        self._loading_id = self._artist_albums.connect(
+            "ready", self._on_artist_albums_ready, coreartist.props.artist)
+        self._view.set_visible_child_name("empty-frame")
+        self._window.notifications_popup.push_loading()
+        return
 
-    @log
-    def _add_item(self, source, param, item, remaining=0, data=None):
-        if (not item and remaining == 0):
-            self._window.notifications_popup.pop_loading()
-            self._sidebar.show()
-            return
-        self._offset += 1
-        artist = utils.get_artist_name(item)
-        row = None
-        if not artist.casefold() in self._artists:
-            # populate sidebar
-            row = SidebarRow()
-            row.props.text = artist
-            row.connect('notify::selected', self._on_selection_changed)
-            self.bind_property('selection-mode', row, 'selection-mode')
-            self._sidebar.add(row)
-
-            self._artists[artist.casefold()] = {
-                'albums': [],
-                'widget': None
-            }
-
-        self._artists[artist.casefold()]['albums'].append(item)
-
-        if (row is not None
-                and len(self._sidebar) == 1):
-            self._sidebar.select_row(row)
-            self._sidebar.emit('row-activated', row)
+    def _on_artist_albums_ready(self, widget, artist):
+        artist_albums_frame = Gtk.Frame(
+            shadow_type=Gtk.ShadowType.NONE, hexpand=True)
+        artist_albums_frame.add(self._artist_albums)
+        artist_albums_frame.show()
+
+        self._view.add_named(artist_albums_frame, artist)
+        scroll_vadjustment = self._view_container.props.vadjustment
+        scroll_vadjustment.props.value = 0.
+        self._view.set_visible_child(artist_albums_frame)
+        self._window.notifications_popup.pop_loading()
+        self._loaded_artists.append(artist)
+
+        self._artist_albums.disconnect(self._loading_id)
+        self._loading_id = 0
+        self._artist_albums = None
+        return
 
     @log
     def _populate(self, data=None):
         """Populates the view"""
-        self._window.notifications_popup.push_loading()
-        grilo.populate_artists(self._offset, self._add_item)
-        self._init = True
+        pass
 
     @log
     def _on_sidebar_clicked(self, gesture, n_press, x, y):
@@ -183,12 +207,7 @@ class ArtistsView(BaseView):
 
     @log
     def _on_selection_changed(self, widget, value, data=None):
-        selected_artists = 0
-        for row in self._sidebar:
-            if row.props.selected:
-                selected_artists += 1
-
-        self.props.selected_items_count = selected_artists
+        return
 
     @log
     def _on_selection_mode_changed(self, widget, data=None):
@@ -200,14 +219,14 @@ class ArtistsView(BaseView):
         else:
             self._sidebar.props.selection_mode = Gtk.SelectionMode.SINGLE
 
-        if (not self.props.selection_mode
-                and grilo.changes_pending['Artists']):
-            self._on_changes_pending()
-
     @log
     def _toggle_all_selection(self, selected):
-        for row in self._sidebar:
-            row.props.selected = selected
+
+        def toggle_selection(child):
+            tile = child.get_child()
+            tile.props.selected = selected
+
+        self._sidebar.foreach(toggle_selection)
 
     @log
     def select_all(self):
@@ -216,39 +235,3 @@ class ArtistsView(BaseView):
     @log
     def unselect_all(self):
         self._toggle_all_selection(False)
-
-    @log
-    def get_selected_songs(self, callback):
-        """Returns a list of songs selected
-
-        In this view this will be all albums of the selected artists.
-        :returns: All selected songs
-        :rtype: A list of songs
-        """
-        selected_albums = []
-        for row in self._sidebar:
-            if row.props.selected:
-                artist = row.props.text
-                albums = self._artists[artist.casefold()]['albums']
-                selected_albums.extend(albums)
-
-        if len(selected_albums) > 0:
-            self._get_selected_albums_songs(selected_albums, callback)
-
-    @log
-    def _get_selected_albums_songs(self, albums, callback):
-        selected_songs = []
-        self._album_index = 0
-
-        def add_songs(source, param, item, remaining, data=None):
-            if item:
-                selected_songs.append(item)
-            if remaining == 0:
-                self._album_index += 1
-                if self._album_index < len(albums):
-                    grilo.populate_album_songs(
-                        albums[self._album_index], add_songs)
-                else:
-                    callback(selected_songs)
-
-        grilo.populate_album_songs(albums[self._album_index], add_songs)
diff --git a/gnomemusic/views/baseview.py b/gnomemusic/views/baseview.py
index c1794c9b..ed0935d9 100644
--- a/gnomemusic/views/baseview.py
+++ b/gnomemusic/views/baseview.py
@@ -22,10 +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 gi.repository import GdkPixbuf, GObject, Gtk
+from gi.repository import GObject, Gtk
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
 from gnomemusic.widgets.starhandlerwidget import StarHandlerWidget
 
 
@@ -54,20 +53,6 @@ class BaseView(Gtk.Stack):
 
         self._grid = Gtk.Grid(orientation=Gtk.Orientation.HORIZONTAL)
         self._offset = 0
-        self.model = Gtk.ListStore(
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GdkPixbuf.Pixbuf,
-            GObject.TYPE_OBJECT,
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT,
-            GObject.TYPE_STRING,
-            GObject.TYPE_INT,
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT
-        )
         self._box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
 
         # Setup the main view
@@ -78,7 +63,7 @@ class BaseView(Gtk.Stack):
 
         self._grid.add(self._box)
 
-        self._star_handler = StarHandlerWidget(self, 9)
+        self._star_handler = StarHandlerWidget(self, 6)
         self._window = window
         self._headerbar = window._headerbar
 
@@ -87,19 +72,17 @@ class BaseView(Gtk.Stack):
 
         self.add(self._grid)
         self.show_all()
-        self._view.hide()
+        # self._view.hide()
 
         self._init = False
-        grilo.connect('ready', self._on_grilo_ready)
-        self.connect('notify::selection-mode', self._on_selection_mode_changed)
-        grilo.connect('changes-pending', self._on_changes_pending)
+        self._selection_mode_id = self.connect(
+            "notify::selection-mode", self._on_selection_mode_changed)
 
         self.bind_property(
             'selection-mode', self._window, 'selection-mode',
             GObject.BindingFlags.BIDIRECTIONAL)
 
-        if (grilo.tracker is not None
-                and not self._init):
+        if not self._init:
             self._on_grilo_ready()
 
     @log
@@ -135,10 +118,6 @@ class BaseView(Gtk.Stack):
         if not self.props.selection_mode:
             self.unselect_all()
 
-    @log
-    def _retrieval_finished(self, klass):
-        self.model[klass.iter][4] = klass.pixbuf
-
     @log
     def _on_item_activated(self, widget, id, path):
         pass
@@ -146,28 +125,3 @@ class BaseView(Gtk.Stack):
     @log
     def get_selected_songs(self, callback):
         callback([])
-
-    @log
-    def _set_selection(self, value, parent=None):
-        count = 0
-        itr = self.model.iter_children(parent)
-        while itr is not None:
-            if self.model.iter_has_child(itr):
-                count += self._set_selection(value, itr)
-            if self.model[itr][5] is not None:
-                self.model[itr][6] = value
-                count += 1
-            itr = self.model.iter_next(itr)
-
-        return count
-
-    @log
-    def select_all(self):
-        """Select all the available songs."""
-        self.props.selected_items_count = self._set_selection(True)
-
-    @log
-    def unselect_all(self):
-        """Unselects all the selected songs."""
-        self._set_selection(False)
-        self.props.selected_items_count = 0
diff --git a/gnomemusic/views/emptyview.py b/gnomemusic/views/emptyview.py
index 2a72fb58..a9ae13da 100644
--- a/gnomemusic/views/emptyview.py
+++ b/gnomemusic/views/emptyview.py
@@ -25,11 +25,10 @@
 from enum import IntEnum
 
 from gettext import gettext as _
-from gi.repository import GObject, Gtk
+from gi.repository import GLib, GObject, Gtk, Tracker
 
 from gnomemusic import log
 from gnomemusic.albumartcache import Art
-from gnomemusic.query import Query
 
 
 @Gtk.Template(resource_path="/org/gnome/Music/ui/EmptyView.ui")
@@ -63,8 +62,20 @@ class EmptyView(Gtk.Stack):
     def __init__(self):
         super().__init__()
 
+        # FIXME: This is now duplicated here and in GrlTrackerWrapper.
+        try:
+            music_folder = GLib.get_user_special_dir(
+                GLib.UserDirectory.DIRECTORY_MUSIC)
+            assert music_folder is not None
+        except (TypeError, AssertionError):
+            print("XDG Music dir is not set")
+            return
+
+        music_folder = Tracker.sparql_escape_string(
+            GLib.filename_to_uri(music_folder))
+
         href_text = "<a href='{}'>{}</a>".format(
-            Query.MUSIC_URI, _("Music folder"))
+            music_folder, _("Music folder"))
 
         # TRANSLATORS: This is a label to display a link to open user's music
         # folder. {} will be replaced with the translated text 'Music folder'
diff --git a/gnomemusic/views/playlistsview.py b/gnomemusic/views/playlistsview.py
index c15099de..bcab1ba1 100644
--- a/gnomemusic/views/playlistsview.py
+++ b/gnomemusic/views/playlistsview.py
@@ -24,19 +24,17 @@
 
 from gettext import gettext as _
 
-from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
+from gi.repository import Gdk, GObject, Gio, Gtk
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
-from gnomemusic.player import ValidationStatus, PlayerPlaylist
-from gnomemusic.playlists import Playlists
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.views.baseview import BaseView
 from gnomemusic.widgets.notificationspopup import PlaylistNotification
 from gnomemusic.widgets.playlistcontextmenu import PlaylistContextMenu
 from gnomemusic.widgets.playlistcontrols import PlaylistControls
 from gnomemusic.widgets.playlistdialog import PlaylistDialog
-from gnomemusic.widgets.sidebarrow import SidebarRow
-import gnomemusic.utils as utils
+from gnomemusic.widgets.playlisttile import PlaylistTile
+from gnomemusic.widgets.songwidget import SongWidget
 
 
 class PlaylistsView(BaseView):
@@ -59,15 +57,12 @@ class PlaylistsView(BaseView):
         super().__init__(
             'playlists', _("Playlists"), window, sidebar_container)
 
+        self._coremodel = window._app.props.coremodel
+        self._model = self._coremodel.props.playlists_sort
         self._window = window
         self.player = player
 
-        self._view.get_style_context().add_class('songs-list')
-
-        self._add_list_renderers()
-
         self._pl_ctrls = PlaylistControls()
-        self._pl_ctrls.connect('playlist-renamed', self._on_playlist_renamed)
 
         self._song_popover = PlaylistContextMenu(self._view)
 
@@ -86,7 +81,8 @@ class PlaylistsView(BaseView):
         self._window.add_action(self._remove_song_action)
 
         playlist_play_action = Gio.SimpleAction.new('playlist_play', None)
-        playlist_play_action.connect('activate', self._on_play_activate)
+        playlist_play_action.connect(
+            'activate', self._on_play_playlist)
         self._window.add_action(playlist_play_action)
 
         self._playlist_delete_action = Gio.SimpleAction.new(
@@ -94,6 +90,7 @@ class PlaylistsView(BaseView):
         self._playlist_delete_action.connect(
             'activate', self._stage_playlist_for_deletion)
         self._window.add_action(self._playlist_delete_action)
+
         self._playlist_rename_action = Gio.SimpleAction.new(
             'playlist_rename', None)
         self._playlist_rename_action.connect(
@@ -111,56 +108,29 @@ class PlaylistsView(BaseView):
         self._grid.child_set_property(sidebar_container, 'top-attach', 0)
         self._grid.child_set_property(sidebar_container, 'height', 2)
 
-        self._iter_to_clean = None
-        self._iter_to_clean_model = None
-        self._current_playlist = None
-        self._plays_songs_on_activation = False
-        self._songs_todelete = {}
-        self._songs_count = 0
-
-        self.model.connect('row-inserted', self._on_song_inserted)
-        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)
-
-        self._playlists = Playlists.get_default()
-        self._playlists.connect("notify::ready", self._on_playlists_loading)
-        self._playlists.connect("playlist-updated", self._on_playlist_update)
-        self._playlists.connect(
-            "song-added-to-playlist", self._on_song_added_to_playlist)
-        self._playlists.connect(
+        self._sidebar.bind_model(self._model, self._add_playlist_to_sidebar)
+
+        self._loaded_id = self._coremodel.connect(
+            "playlists-loaded", self._on_playlists_loaded)
+        self._coremodel.connect(
             "activate-playlist", self._on_playlist_activation_request)
 
-        self._playlists_model = self._playlists.get_playlists_model()
-        self._sidebar.bind_model(
-            self._playlists_model, self._add_playlist_to_sidebar)
-        self._playlists_model.connect(
-            "items-changed", self._on_playlists_model_changed)
+        # Selection is only possible from the context menu
+        self.disconnect(self._selection_mode_id)
 
         self.show_all()
 
-    @log
-    def _update_songs_count(self, songs_count):
-        self._songs_count = songs_count
-        self._pl_ctrls.props.songs_count = songs_count
-
     @log
     def _setup_view(self):
         view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
         self._box.pack_start(view_container, True, True, 0)
 
-        self._view = Gtk.TreeView()
-        self._view.set_headers_visible(False)
-        self._view.set_valign(Gtk.Align.START)
-        self._view.set_model(self.model)
-        self._view.set_activate_on_single_click(True)
-        self._view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
-
-        self._view.connect('row-activated', self._on_song_activated)
-        self._view.connect('drag-begin', self._drag_begin)
-        self._view.connect('drag-end', self._drag_end)
-        self._song_drag = {'active': False}
+        self._view = Gtk.ListBox()
+        self._view.get_style_context().add_class("songs-list")
+        self._view.props.margin_top = 20
+        self._view.props.margin_left = 80
+        self._view.props.margin_right = 80
+        self._view.props.valign = Gtk.Align.START
 
         self._controller = Gtk.GestureMultiPress().new(self._view)
         self._controller.props.propagation_phase = Gtk.PropagationPhase.CAPTURE
@@ -169,115 +139,6 @@ class PlaylistsView(BaseView):
 
         view_container.add(self._view)
 
-    @log
-    def _add_list_renderers(self):
-        now_playing_symbol_renderer = Gtk.CellRendererPixbuf(
-            xpad=0, xalign=0.5, yalign=0.5)
-        column_now_playing = Gtk.TreeViewColumn()
-        column_now_playing.set_fixed_width(48)
-        column_now_playing.pack_start(now_playing_symbol_renderer, False)
-        column_now_playing.set_cell_data_func(
-            now_playing_symbol_renderer, self._on_list_widget_icon_render,
-            None)
-        self._view.append_column(column_now_playing)
-
-        title_renderer = Gtk.CellRendererText(
-            xpad=0, xalign=0.0, yalign=0.5, height=48,
-            ellipsize=Pango.EllipsizeMode.END)
-        column_title = Gtk.TreeViewColumn("Title", title_renderer, text=2)
-        column_title.set_expand(True)
-        self._view.append_column(column_title)
-
-        column_star = Gtk.TreeViewColumn()
-        self._view.append_column(column_star)
-        self._star_handler.add_star_renderers(column_star)
-
-        duration_renderer = Gtk.CellRendererText(xpad=32, xalign=1.0)
-        column_duration = Gtk.TreeViewColumn()
-        column_duration.pack_start(duration_renderer, False)
-        column_duration.set_cell_data_func(
-            duration_renderer, self._on_list_widget_duration_render, None)
-        self._view.append_column(column_duration)
-
-        artist_renderer = Gtk.CellRendererText(
-            xpad=32, ellipsize=Pango.EllipsizeMode.END)
-        column_artist = Gtk.TreeViewColumn("Artist", artist_renderer, text=3)
-        column_artist.set_expand(True)
-        self._view.append_column(column_artist)
-
-        album_renderer = Gtk.CellRendererText(
-            xpad=32, ellipsize=Pango.EllipsizeMode.END)
-        column_album = Gtk.TreeViewColumn()
-        column_album.set_expand(True)
-        column_album.pack_start(album_renderer, True)
-        column_album.set_cell_data_func(
-            album_renderer, self._on_list_widget_album_render, None)
-        self._view.append_column(column_album)
-
-    def _on_list_widget_duration_render(self, col, cell, model, _iter, data):
-        if not model.iter_is_valid(_iter):
-            return
-
-        item = model[_iter][5]
-        if item:
-            duration = item.get_duration()
-            cell.set_property('text', utils.seconds_to_string(duration))
-
-    def _on_list_widget_album_render(self, coll, cell, model, _iter, data):
-        if not model.iter_is_valid(_iter):
-            return
-
-        item = model[_iter][5]
-        if item:
-            cell.set_property('text', utils.get_album_title(item))
-
-    def _on_list_widget_icon_render(self, col, cell, model, _iter, data):
-        playlist_id = self._current_playlist.props.pl_id
-        if not self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_id):
-            cell.set_visible(False)
-            return
-
-        if not model.iter_is_valid(_iter):
-            return
-
-        current_song = self.player.props.current_song
-        if model[_iter][11] == ValidationStatus.FAILED:
-            cell.set_property('icon-name', self._error_icon_name)
-            cell.set_visible(True)
-        elif model[_iter][5].get_id() == current_song.get_id():
-            cell.set_property('icon-name', self._now_playing_icon_name)
-            cell.set_visible(True)
-        else:
-            cell.set_visible(False)
-
-    @log
-    def _update_model(self, player):
-        """Updates model when the song changes
-
-        :param Player player: The main player object
-        """
-        if self._current_playlist is None:
-            return
-
-        playlist_id = self._current_playlist.props.pl_id
-        if self._iter_to_clean:
-            self._iter_to_clean_model[self._iter_to_clean][10] = False
-        if not player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_id):
-            return False
-
-        index = self.player.props.current_song_index
-        iter_ = self.model.get_iter_from_string(str(index))
-        self.model[iter_][10] = True
-        path = self.model.get_path(iter_)
-        self._view.scroll_to_cell(path, None, False, 0., 0.)
-        if self.model[iter_][8] != self._error_icon_name:
-            self._iter_to_clean = iter_.copy()
-            self._iter_to_clean_model = self.model
-
-        return False
-
     @log
     def _add_playlist_to_sidebar(self, playlist):
         """Add a playlist to sidebar
@@ -285,200 +146,85 @@ class PlaylistsView(BaseView):
         :param GrlMedia playlist: playlist to add
         :param int index: position
         """
-        row = SidebarRow()
-        row.props.text = playlist.props.title
-        row.playlist = playlist
-
+        row = PlaylistTile(playlist)
         return row
 
+    def _on_playlists_loaded(self, klass):
+        self._coremodel.disconnect(self._loaded_id)
+        first_row = self._sidebar.get_row_at_index(0)
+        self._sidebar.select_row(first_row)
+        first_row.emit("activate")
+
     def _on_playlists_model_changed(self, model, position, removed, added):
         if removed == 0:
             return
 
-        row_next = (self._sidebar.get_row_at_index(position)
-                    or self._sidebar.get_row_at_index(position - 1))
-        if row_next:
-            self._sidebar.select_row(row_next)
-            row_next.emit("activate")
-
-    @log
-    def _on_song_validated(self, player, index, status):
-        if self._current_playlist is None:
-            return
-
-        playlist_id = self._current_playlist.props.pl_id
-        if not self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_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
-
-        clicking on star column toggles favorite
-        clicking on an other columns launches player
-        Action is not performed if drag and drop is active
-
-        :param Gtk.Tree treeview: self._view
-        :param Gtk.TreePath path: activated row index
-        :param Gtk.TreeViewColumn column: activated column
-        """
-        def activate_song():
-            if self._song_drag['active']:
-                return GLib.SOURCE_REMOVE
-
-            if self._star_handler.star_renderer_click:
-                self._star_handler.star_renderer_click = False
-                return GLib.SOURCE_REMOVE
-
-            _iter = None
-            if path:
-                _iter = self.model.get_iter(path)
-            playlist_id = self._current_playlist.props.pl_id
-            self.player.set_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_id, self.model, _iter)
-            self.player.play()
-
-            return GLib.SOURCE_REMOVE
-
-        # 'row-activated' signal is emitted before 'drag-begin' signal.
-        # Need to wait to check if drag and drop operation is active.
-        GLib.idle_add(activate_song)
-
     @log
     def _on_view_right_clicked(self, gesture, n_press, x, y):
-        (path, column, cell_x, cell_y) = self._view.get_path_at_pos(x, y)
-        self._view.get_selection().select_path(path)
-        row_height = self._view.get_cell_area(path, None).height
+        requested_row = self._view.get_row_at_y(y)
+        self._view.select_row(requested_row)
 
+        _, y0 = requested_row.translate_coordinates(self._view, 0, 0)
+        row_height = requested_row.get_allocated_height()
         rect = Gdk.Rectangle()
         rect.x = x
-        rect.y = y - cell_y + 0.5 * row_height
+        rect.y = y0 + 0.5 * row_height
 
         self._song_popover.props.relative_to = self._view
         self._song_popover.props.pointing_to = rect
         self._song_popover.popup()
 
-    @log
-    def _drag_begin(self, widget_, drag_context):
-        self._song_drag['active'] = True
-
-    @log
-    def _drag_end(self, widget_, drag_context):
-        self._song_drag['active'] = False
-
-    @log
-    def _on_song_inserted(self, model, path, iter_):
-        if not self._song_drag['active']:
-            return
-
-        self._song_drag['new_pos'] = int(path.to_string())
-
-    @log
-    def _on_song_deleted(self, model, path):
-        """Save new playlist order after drag and drop operation.
-
-        Update player's playlist if the playlist is being played.
-        """
-        if not self._song_drag['active']:
-            return
-
-        new_pos = self._song_drag['new_pos']
-        prev_pos = int(path.to_string())
-
-        if abs(new_pos - prev_pos) == 1:
-            return
-
-        first_pos = min(new_pos, prev_pos)
-        last_pos = max(new_pos, prev_pos)
-
-        # update player's playlist if necessary
-        playlist_id = self._current_playlist.props.pl_id
-        if self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_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):
-            _iter = model.get_iter_from_string(str(pos))
-            songs.append(model[_iter][5])
-            positions.append(pos + 1)
-
-        self._playlists.reorder_playlist(
-            self._current_playlist, songs, positions)
-
     @log
     def _play_song(self, menuitem, data=None):
-        model, _iter = self._view.get_selection().get_selected()
-        path = model.get_path(_iter)
-        self._view.emit('row-activated', path, None)
+        selected_row = self._view.get_selected_row()
+        song_widget = selected_row.get_child()
+        self._view.unselect_all()
+        self._song_activated(song_widget)
 
-    @log
     def _add_song_to_playlist(self, menuitem, data=None):
-        model, _iter = self._view.get_selection().get_selected()
-        song = model[_iter][5]
+        selected_row = self._view.get_selected_row()
+        song_widget = selected_row.get_child()
+        coresong = song_widget.props.coresong
 
         playlist_dialog = PlaylistDialog(self._window)
         if playlist_dialog.run() == Gtk.ResponseType.ACCEPT:
-            self._playlists.add_to_playlist(
-                playlist_dialog.get_selected(), [song])
+            playlist = playlist_dialog.props.selected_playlist
+            playlist.add_songs([coresong])
+
+        self._view.unselect_all()
         playlist_dialog.destroy()
 
     @log
     def _stage_song_for_deletion(self, menuitem, data=None):
-        model, _iter = self._view.get_selection().get_selected()
-        song = model[_iter][5]
-        index = int(model.get_path(_iter).to_string())
-        song_id = song.get_id()
-        self._songs_todelete[song_id] = {
-            'playlist': self._current_playlist,
-            'song': song,
-            'index': index
-        }
-        self._remove_song_from_playlist(self._current_playlist, song, index)
-        self._create_notification(PlaylistNotification.Type.SONG, song_id)
+        selected_row = self._view.get_selected_row()
+        position = selected_row.get_index()
+        song_widget = selected_row.get_child()
+        coresong = song_widget.props.coresong
 
-    @log
-    def _on_playlists_loading(self, klass, value):
-        if not self._playlists.props.ready:
-            self._window.notifications_popup.push_loading()
-        else:
-            self._window.notifications_popup.pop_loading()
-            first_row = self._sidebar.get_row_at_index(0)
-            self._sidebar.select_row(first_row)
-            first_row.emit("activate")
+        selection = self._sidebar.get_selected_row()
+        selected_playlist = selection.props.playlist
+
+        notification = PlaylistNotification(  # noqa: F841
+            self._window.notifications_popup, self._coremodel,
+            PlaylistNotification.Type.SONG, selected_playlist, position,
+            coresong)
 
     @log
-    def _on_playlist_update(self, playlists, playlist):
-        """Refresh the displayed playlist if necessary
+    def _on_playlist_activated(self, sidebar, row, data=None):
+        """Update view with content from selected playlist"""
+        playlist = row.props.playlist
 
-        :param playlists: playlists object
-        :param Playlist playlist: updated playlist
-        """
-        if not self._is_current_playlist(playlist):
-            return
+        if self.rename_active:
+            self._pl_ctrls.disable_rename_playlist()
 
-        self._star_handler.star_renderer_click = False
-        for row in self._sidebar:
-            if playlist == row.playlist:
-                self._on_playlist_activated(self._sidebar, row)
-                break
+        self._view.bind_model(
+            playlist.props.model, self._create_song_widget, playlist)
+
+        self._pl_ctrls.props.playlist = playlist
+
+        self._playlist_rename_action.set_enabled(not playlist.props.is_smart)
+        self._playlist_delete_action.set_enabled(not playlist.props.is_smart)
 
-    @log
     def _on_playlist_activation_request(self, klass, playlist):
         """Selects and starts playing a playlist.
 
@@ -486,17 +232,16 @@ class PlaylistsView(BaseView):
         select the requested playlist. Otherwise, directly select the
         requested playlist and start playing.
 
-        :param Playlists klass: Playlists object
+        :param CoreModel klass: Main CorexModel
         :param Playlist playlist: requested playlist
         """
-        if not self._init:
-            self._plays_songs_on_activation = True
-            self._populate(playlist.props.pl_id)
-            return
+        def _on_playlist_loaded(playlist):
+            playlist.disconnect(playlist_ready_id)
+            self._song_activated(None)
 
         playlist_row = None
         for row in self._sidebar:
-            if row.playlist == playlist:
+            if row.props.playlist == playlist:
                 playlist_row = row
                 break
 
@@ -505,192 +250,43 @@ class PlaylistsView(BaseView):
 
         selection = self._sidebar.get_selected_row()
         if selection.get_index() == playlist_row.get_index():
-            self._on_play_activate(None)
-        else:
-            self._plays_songs_on_activation = True
-            self._sidebar.select_row(row)
-            row.emit('activate')
-
-    @log
-    def remove_playlist(self):
-        """Removes the current selected playlist"""
-        if self._current_playlist.props.is_smart:
+            self._song_activated(None)
             return
-        self._stage_playlist_for_deletion(None)
-
-    @log
-    def _on_playlist_activated(self, sidebar, row, data=None):
-        """Update view with content from selected playlist"""
-        playlist = row.playlist
-        playlist_name = playlist.props.title
-
-        if self.rename_active:
-            self._pl_ctrls.disable_rename_playlist()
-
-        self._current_playlist = playlist
-        self._pl_ctrls.props.playlist_name = playlist_name
-
-        # if the active queue has been set by this playlist,
-        # use it as model, otherwise build the liststore
-        self._view.set_model(None)
-        self.model.clear()
-        self._iter_to_clean = None
-        self._iter_to_clean_model = None
-        self._pl_ctrls.freeze_notify()
-        self._update_songs_count(0)
-        grilo.populate_playlist_songs(playlist, self._add_song)
-
-        protected_pl = self._current_playlist.props.is_smart
-        self._playlist_delete_action.set_enabled(not protected_pl)
-        self._playlist_rename_action.set_enabled(not protected_pl)
-        self._remove_song_action.set_enabled(not protected_pl)
-        self._view.set_reorderable(not protected_pl)
-
-    @log
-    def _add_song(self, source, param, song, remaining=0, data=None):
-        """Grilo.populate_playlist_songs callback.
-
-        Add all playlists found by Grilo to self._model
-
-        :param GrlTrackerSource source: tracker source
-        :param int param: param
-        :param GrlMedia song: song to add
-        :param int remaining: next playlist_id or zero if None
-        :param data: associated data
-        """
-        self._add_song_to_model(song, self.model)
-        if remaining == 0:
-            self._view.set_model(self.model)
-            self._pl_ctrls.thaw_notify()
-            if self._plays_songs_on_activation:
-                first_iter = self.model.get_iter_first()
-                self.player.set_playlist(
-                    PlayerPlaylist.Type.PLAYLIST,
-                    self._current_playlist.props.pl_id, self.model, first_iter)
-                self.player.play()
-                self._plays_songs_on_activation = False
-
-    @log
-    def _add_song_to_model(self, song, model, index=-1):
-        """Add song to a playlist
-        :param Grl.Media song: song to add
-        :param Gtk.ListStore model: model
-        """
-        if not song:
-            return None
-
-        title = utils.get_media_title(song)
-        song.set_title(title)
-        artist = utils.get_artist_name(song)
-        iter_ = model.insert_with_valuesv(
-            index, [2, 3, 5, 9],
-            [title, artist, song, song.get_favourite()])
-
-        self._update_songs_count(self._songs_count + 1)
-        return iter_
-
-    @log
-    def _on_play_activate(self, menuitem, data=None):
-        self._view.emit('row-activated', None, None)
-
-    @log
-    def _is_current_playlist(self, playlist):
-        """Check if playlist is currently displayed"""
-        if self._current_playlist is None:
-            return False
-
-        return playlist.props.pl_id == self._current_playlist.props.pl_id
-
-    @log
-    def _get_removal_notification_message(self, type_, data):
-        """ Returns a label for the playlist notification popup
-
-        Handles two cases:
-        - playlist removal
-        - songs from playlist removal
-        """
-        msg = ""
-
-        if type_ == PlaylistNotification.Type.PLAYLIST:
-            pl_todelete = data
-            msg = _("Playlist {} removed".format(pl_todelete.props.title))
 
+        self._sidebar.select_row(row)
+        row.emit('activate')
+        if playlist.props.model.get_n_items() > 0:
+            self._song_activated(None)
         else:
-            song_id = data
-            song_todelete = self._songs_todelete[song_id]
-            playlist_title = song_todelete["playlist"].props.title
-            song_title = utils.get_media_title(song_todelete['song'])
-            msg = _("{} removed from {}".format(
-                song_title, playlist_title))
-
-        return msg
-
-    @log
-    def _create_notification(self, type_, data):
-        msg = self._get_removal_notification_message(type_, data)
-        playlist_notification = PlaylistNotification(
-            self._window.notifications_popup, type_, msg, data)
-        playlist_notification.connect(
-            'undo-deletion', self._undo_pending_deletion)
-        playlist_notification.connect(
-            'finish-deletion', self._finish_pending_deletion)
-
-    @log
-    def _stage_playlist_for_deletion(self, menutime, data=None):
-        self.model.clear()
-        selection = self._sidebar.get_selected_row()
-        index = selection.get_index()
-        playlist_id = selection.playlist.props.pl_id
-        self._playlists.stage_playlist_for_deletion(selection.playlist, index)
-
-        if self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_id):
-            self.player.stop()
-            self._window.set_player_visible(False)
-
-        self._create_notification(
-            PlaylistNotification.Type.PLAYLIST, selection.playlist)
+            playlist_ready_id = playlist.connect(
+                "playlist-loaded", _on_playlist_loaded)
 
-    @log
-    def _undo_pending_deletion(self, playlist_notification):
-        """Revert the last playlist removal"""
-        notification_type = playlist_notification.type_
+    def _create_song_widget(self, coresong, playlist):
+        can_dnd = not playlist.props.is_smart
+        song_widget = SongWidget(coresong, can_dnd, True)
+        song_widget.props.show_song_number = False
 
-        if notification_type == PlaylistNotification.Type.PLAYLIST:
-            pl_todelete = playlist_notification.data
-            self._playlists.undo_pending_deletion(pl_todelete)
+        song_widget.connect('button-release-event', self._song_activated)
+        if can_dnd is True:
+            song_widget.connect("widget_moved", self._on_song_widget_moved)
 
-        else:
-            song_id = playlist_notification.data
-            song_todelete = self._songs_todelete[song_id]
-            self._songs_todelete.pop(song_id)
-            if not self._is_current_playlist(song_todelete['playlist']):
-                return
+        return song_widget
 
-            iter_ = self._add_song_to_model(
-                song_todelete['song'], self.model, song_todelete['index'])
+    def _song_activated(self, widget=None, event=None):
+        coresong = None
+        if widget is not None:
+            coresong = widget.props.coresong
 
-            playlist_id = self._current_playlist.props.pl_id
-            if not self.player.playing_playlist(
-                    PlayerPlaylist.Type.PLAYLIST, playlist_id):
-                return
+        selection = self._sidebar.get_selected_row()
+        current_playlist = selection.props.playlist
+        self._coremodel.set_playlist_model(
+            PlayerPlaylist.Type.PLAYLIST, current_playlist.props.model)
+        self.player.play(coresong)
 
-            path = self.model.get_path(iter_)
-            self.player.add_song(self.model[iter_][5], int(path.to_string()))
+        return True
 
-    @log
-    def _finish_pending_deletion(self, playlist_notification):
-        notification_type = playlist_notification.type_
-
-        if notification_type == PlaylistNotification.Type.PLAYLIST:
-            pl_todelete = playlist_notification.data
-            self._playlists.delete_playlist(pl_todelete)
-        else:
-            song_id = playlist_notification.data
-            song_todelete = self._songs_todelete[song_id]
-            self._playlists.remove_from_playlist(
-                song_todelete['playlist'], [song_todelete['song']])
-            self._songs_todelete.pop(song_id)
+    def _on_play_playlist(self, menuitem, data=None):
+        self._song_activated()
 
     @GObject.Property(type=bool, default=False)
     def rename_active(self):
@@ -700,44 +296,36 @@ class PlaylistsView(BaseView):
     @log
     def _stage_playlist_for_renaming(self, menuitem, data=None):
         selection = self._sidebar.get_selected_row()
-        pl_torename = selection.playlist
+        pl_torename = selection.props.playlist
         self._pl_ctrls.enable_rename_playlist(pl_torename)
 
     @log
     def _on_playlist_renamed(self, arguments, new_name):
         selection = self._sidebar.get_selected_row()
-        selection.props.text = new_name
-
-        pl_torename = selection.playlist
-        pl_torename.props.title = new_name
-        self._playlists.rename(pl_torename, new_name)
+        pl_torename = selection.props.playlist
+        pl_torename.rename(new_name)
 
     @log
-    def _on_song_added_to_playlist(self, playlists, playlist, item):
-        if not self._is_current_playlist(playlist):
-            return
-
-        iter_ = self._add_song_to_model(item, self.model)
-        playlist_id = self._current_playlist.props.pl_id
-        if self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_id):
-            path = self.model.get_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
-
-        playlist_id = self._current_playlist.props.pl_id
-        if self.player.playing_playlist(
-                PlayerPlaylist.Type.PLAYLIST, playlist_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)
+    def _stage_playlist_for_deletion(self, menutime, data=None):
+        selected_row = self._sidebar.get_selected_row()
+        selected_playlist = selected_row.props.playlist
+
+        notification = PlaylistNotification(  # noqa: F841
+            self._window.notifications_popup, self._coremodel,
+            PlaylistNotification.Type.PLAYLIST, selected_playlist)
+
+        # FIXME: Should Check that the playlist is not playing
+        # playlist_id = selection.playlist.props.pl_id
+        # if self.player.playing_playlist(
+        #         PlayerPlaylist.Type.PLAYLIST, playlist_id):
+        #     self.player.stop()
+        #     self._window.set_player_visible(False)
+
+    def _on_song_widget_moved(self, target, source_position):
+        target_position = target.get_parent().get_index()
+        selection = self._sidebar.get_selected_row()
+        current_playlist = selection.props.playlist
+        current_playlist.reorder(source_position, target_position)
 
     @log
     def _populate(self, data=None):
diff --git a/gnomemusic/views/searchview.py b/gnomemusic/views/searchview.py
index 4e33e306..6eed8afa 100644
--- a/gnomemusic/views/searchview.py
+++ b/gnomemusic/views/searchview.py
@@ -22,23 +22,19 @@
 # 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 gettext import gettext as _
-import gi
-gi.require_version('Gd', '1.0')
-from gi.repository import Gd, Gdk, GdkPixbuf, GObject, Grl, Gtk, Pango
+from gi.repository import Gdk, GObject, Gtk
 
-from gnomemusic.albumartcache import Art
-from gnomemusic.grilo import grilo
 from gnomemusic import log
-from gnomemusic.player import ValidationStatus, PlayerPlaylist
-from gnomemusic.query import Query
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.utils import View
 from gnomemusic.search import Search
 from gnomemusic.views.baseview import BaseView
-from gnomemusic.widgets.headerbar import HeaderBar
+from gnomemusic.widgets.albumcover import AlbumCover
 from gnomemusic.widgets.albumwidget import AlbumWidget
+from gnomemusic.widgets.headerbar import HeaderBar
 from gnomemusic.widgets.artistalbumswidget import ArtistAlbumsWidget
-import gnomemusic.utils as utils
+from gnomemusic.widgets.artisttile import ArtistTile
+from gnomemusic.widgets.songwidget import SongWidget
 
 
 class SearchView(BaseView):
@@ -50,134 +46,218 @@ class SearchView(BaseView):
 
     @log
     def __init__(self, window, player):
+        self._coremodel = window._app.props.coremodel
+        self._model = self._coremodel.props.songs_search
+        self._album_model = self._coremodel.props.albums_search
+        self._artist_model = self._coremodel.props.artists_search
         super().__init__('search', None, window)
 
-        self._add_list_renderers()
         self.player = player
-        self._head_iters = [None, None, None, None]
-        self._filter_model = None
 
         self.previous_view = None
 
-        self._albums_selected = []
-        self._albums = {}
-        self._albums_index = 0
-
-        self._album_widget = AlbumWidget(player)
+        self._album_widget = AlbumWidget(player, self)
         self._album_widget.bind_property(
             "selection-mode", self, "selection-mode",
             GObject.BindingFlags.BIDIRECTIONAL)
-        self._album_widget.bind_property(
-            "selected-items-count", self, "selected-items-count")
 
         self.add(self._album_widget)
 
-        self._artists_albums_selected = []
-        self._artists_albums_index = 0
-        self._artists = {}
         self._artist_albums_widget = None
 
-        self._items_selected = []
-        self._items_selected_callback = None
-
-        self._items_found = None
-
         self._search_mode_active = False
-        self.connect("notify::search-state", self._on_search_state_changed)
+        # self.connect("notify::search-state", self._on_search_state_changed)
 
     @log
     def _setup_view(self):
         view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
         self._box.pack_start(view_container, True, True, 0)
 
-        self._view = Gtk.TreeView(
-            activate_on_single_click=True, can_focus=False,
-            halign=Gtk.Align.CENTER, headers_visible=False,
-            show_expanders=False, width_request=530)
-        self._view.get_style_context().add_class('view')
-        self._view.get_style_context().add_class('content-view')
-        self._view.get_selection().props.mode = Gtk.SelectionMode.NONE
-        self._view.connect('row-activated', self._on_item_activated)
+        self._songs_listbox = Gtk.ListBox()
+        self._songs_listbox.bind_model(self._model, self._create_song_widget)
 
-        self._ctrl = Gtk.GestureMultiPress().new(self._view)
-        self._ctrl.props.propagation_phase = Gtk.PropagationPhase.CAPTURE
-        self._ctrl.connect("released", self._on_view_clicked)
+        self._album_flowbox = Gtk.FlowBox(
+            homogeneous=True, hexpand=True, halign=Gtk.Align.FILL,
+            valign=Gtk.Align.START, selection_mode=Gtk.SelectionMode.NONE,
+            margin=18, row_spacing=12, column_spacing=6,
+            min_children_per_line=1, max_children_per_line=20, visible=True)
+        self._album_flowbox.get_style_context().add_class('content-view')
+        self._album_flowbox.bind_model(
+            self._album_model, self._create_album_widget)
+        self._album_flowbox.connect(
+            "child-activated", self._on_album_activated)
 
-        view_container.add(self._view)
+        self._artist_listbox = Gtk.ListBox()
+        self._artist_listbox.bind_model(
+            self._artist_model, self._create_artist_widget)
 
-    @log
-    def _back_button_clicked(self, widget, data=None):
-        if self.get_visible_child() == self._artist_albums_widget:
-            self._artist_albums_widget.destroy()
-            self._artist_albums_widget = None
-        elif self.get_visible_child() == self._grid:
-            self._window.views[View.ALBUM].set_visible_child(
-                self._window.views[View.ALBUM]._grid)
+        self._all_results_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self._all_results_box.pack_start(self._album_flowbox, True, True, 0)
+        self._all_results_box.pack_start(self._artist_listbox, True, True, 0)
+        self._all_results_box.pack_start(self._songs_listbox, True, True, 0)
 
-        self.set_visible_child(self._grid)
-        self.props.search_mode_active = True
-        self._headerbar.props.state = HeaderBar.State.MAIN
+        # self._ctrl = Gtk.GestureMultiPress().new(self._view)
+        # self._ctrl.props.propagation_phase = Gtk.PropagationPhase.CAPTURE
+        # self._ctrl.connect("released", self._on_view_clicked)
 
-    @log
-    def _on_item_activated(self, treeview, path, column):
-        if self._star_handler.star_renderer_click:
-            self._star_handler.star_renderer_click = False
+        view_container.add(self._all_results_box)
+
+        self._box.show_all()
+
+    def _create_song_widget(self, coresong):
+        song_widget = SongWidget(coresong)
+
+        self.bind_property(
+            "selection-mode", song_widget, "selection-mode",
+            GObject.BindingFlags.BIDIRECTIONAL
+            | GObject.BindingFlags.SYNC_CREATE)
+
+        song_widget.connect('button-release-event', self._song_activated)
+
+        song_widget.show_all()
+
+        return song_widget
+
+    def _create_album_widget(self, corealbum):
+        album_widget = AlbumCover(corealbum)
+
+        self.bind_property(
+            "selection-mode", album_widget, "selection-mode",
+            GObject.BindingFlags.SYNC_CREATE
+            | GObject.BindingFlags.BIDIRECTIONAL)
+
+        # NOTE: Adding SYNC_CREATE here will trigger all the nested
+        # models to be created. This will slow down initial start,
+        # but will improve initial 'selecte all' speed.
+        album_widget.bind_property(
+            "selected", corealbum, "selected",
+            GObject.BindingFlags.BIDIRECTIONAL)
+
+        return album_widget
+
+    def _create_artist_widget(self, coreartist):
+        artist_tile = ArtistTile(coreartist)
+        artist_tile.props.text = coreartist.props.artist
+        artist_tile.connect('button-release-event', self._artist_activated)
+
+        self.bind_property(
+            "selection-mode", artist_tile, "selection-mode",
+            GObject.BindingFlags.SYNC_CREATE
+            | GObject.BindingFlags.BIDIRECTIONAL)
+
+        return artist_tile
+
+    def _song_activated(self, widget, event):
+        mod_mask = Gtk.accelerator_get_default_mod_mask()
+        if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK
+                and not self.props.selection_mode):
+            self.props.selection_mode = True
             return
 
+        (_, button) = event.get_button()
+        if (button == Gdk.BUTTON_PRIMARY
+                and not self.props.selection_mode):
+            # self.emit('song-activated', widget)
+
+            self._coremodel.set_playlist_model(
+                PlayerPlaylist.Type.SEARCH_RESULT, self._model)
+            self.player.play(widget.props.coresong)
+
+        # FIXME: Need to ignore the event from the checkbox.
+        # if self.props.selection_mode:
+        #     widget.props.selected = not widget.props.selected
+
+        return True
+
+    def _on_album_activated(self, widget, child, user_data=None):
+        corealbum = child.props.corealbum
         if self.props.selection_mode:
             return
 
-        try:
-            child_path = self._filter_model.convert_path_to_child_path(path)
-        except TypeError:
-            return
+        # Update and display the album widget if not in selection mode
+        self._album_widget.update(corealbum)
 
-        _iter = self.model.get_iter(child_path)
-        if self.model[_iter][12] == 'album':
-            title = self.model[_iter][2]
-            artist = self.model[_iter][3]
-            item = self.model[_iter][5]
+        self._headerbar.props.state = HeaderBar.State.SEARCH
+        self._headerbar.props.title = corealbum.props.title
+        self._headerbar.props.subtitle = corealbum.props.artist
+        self.props.search_mode_active = False
 
-            self._album_widget.update(item)
-            self._headerbar.props.state = HeaderBar.State.SEARCH
+        self.set_visible_child(self._album_widget)
 
-            self._headerbar.props.title = title
-            self._headerbar.props.subtitle = artist
-            self.set_visible_child(self._album_widget)
-            self.props.search_mode_active = False
+    def _artist_activated(self, widget, event):
+        coreartist = widget.coreartist
+
+        mod_mask = Gtk.accelerator_get_default_mod_mask()
+        if ((event.get_state() & mod_mask) == Gdk.ModifierType.CONTROL_MASK
+                and not self.props.selection_mode):
+            self.props.selection_mode = True
+            return
 
-        elif self.model[_iter][12] == 'artist':
-            artist = self.model[_iter][2]
-            albums = self._artists[artist.casefold()]['albums']
+        (_, button) = event.get_button()
+        if (button == Gdk.BUTTON_PRIMARY
+                and not self.props.selection_mode):
+            # self.emit('song-activated', widget)
 
             self._artist_albums_widget = ArtistAlbumsWidget(
-                artist, albums, self.player, self._window, True)
+                coreartist, self.player, self._window, False)
             self.add(self._artist_albums_widget)
             self._artist_albums_widget.show()
 
-            self._artist_albums_widget.bind_property(
-                'selected-items-count', self, 'selected-items-count')
             self.bind_property(
                 'selection-mode', self._artist_albums_widget, 'selection-mode',
                 GObject.BindingFlags.BIDIRECTIONAL)
 
             self._headerbar.props.state = HeaderBar.State.SEARCH
-            self._headerbar.props.title = artist
+            self._headerbar.props.title = coreartist.artist
             self._headerbar.props.subtitle = None
             self.set_visible_child(self._artist_albums_widget)
             self.props.search_mode_active = False
-        elif self.model[_iter][12] == 'song':
-            if self.model[_iter][11] != ValidationStatus.FAILED:
-                c_iter = self._songs_model.convert_child_iter_to_iter(_iter)[1]
-                self.player.set_playlist(
-                    PlayerPlaylist.Type.SEARCH_RESULT, None, self._songs_model,
-                    c_iter)
-                self.player.play()
-        else:  # Headers
-            if self._view.row_expanded(path):
-                self._view.collapse_row(path)
-            else:
-                self._view.expand_row(path, False)
+
+        # FIXME: Need to ignore the event from the checkbox.
+        # if self.props.selection_mode:
+        #     widget.props.selected = not widget.props.selected
+
+        return True
+
+    def _child_select(self, child, value):
+        widget = child.get_child()
+        widget.props.selected = value
+
+    def _select_all(self, value):
+        with self._model.freeze_notify():
+            def song_select(child):
+                song_widget = child.get_child()
+                song_widget.props.selected = value
+
+            def album_select(child):
+                child.props.selected = value
+
+            def artist_select(child):
+                artist_widget = child.get_child()
+                artist_widget.props.selected = value
+
+            self._songs_listbox.foreach(song_select)
+            self._album_flowbox.foreach(album_select)
+            self._artist_listbox.foreach(artist_select)
+
+    def select_all(self):
+        self._select_all(True)
+
+    def unselect_all(self):
+        self._select_all(False)
+
+    @log
+    def _back_button_clicked(self, widget, data=None):
+        if self.get_visible_child() == self._artist_albums_widget:
+            self._artist_albums_widget.destroy()
+            self._artist_albums_widget = None
+        elif self.get_visible_child() == self._grid:
+            self._window.views[View.ALBUM].set_visible_child(
+                self._window.views[View.ALBUM]._grid)
+
+        self.set_visible_child(self._grid)
+        self.props.search_mode_active = True
+        self._headerbar.props.state = HeaderBar.State.MAIN
 
     @log
     def _on_view_clicked(self, gesture, n_press, x, y):
@@ -197,26 +277,10 @@ class SearchView(BaseView):
 
             self.props.selected_items_count = len(selected_iters)
 
-    @log
-    def _get_selected_iters(self):
-        iters = []
-        for row in self.model:
-            iter_child = self.model.iter_children(row.iter)
-            while iter_child is not None:
-                if self.model[iter_child][6]:
-                    iters.append(iter_child)
-                iter_child = self.model.iter_next(iter_child)
-        return iters
-
     @log
     def _on_selection_mode_changed(self, widget, data=None):
         super()._on_selection_mode_changed(widget, data)
 
-        col = self._view.get_columns()[0]
-        cells = col.get_cells()
-        cells[4].props.visible = self.props.selection_mode
-        col.queue_resize()
-
     @log
     def _on_search_state_changed(self, klass, param):
         # If a search is triggered when selection mode is activated,
@@ -248,393 +312,7 @@ class SearchView(BaseView):
                 and self.get_visible_child() == self._grid):
             self.props.search_state = Search.State.NONE
 
-    @log
-    def _add_search_item(self, source, param, item, remaining=0, data=None):
-        if not item:
-            if (grilo._search_callback_counter == 0
-                    and grilo.search_source):
-                self.props.search_state = Search.State.NO_RESULT
-            return
-
-        if data != self.model:
-            return
-
-        artist = utils.get_artist_name(item)
-        album = utils.get_album_title(item)
-
-        key = '%s-%s' % (artist, album)
-        if key not in self._albums:
-            self._albums[key] = Grl.Media()
-            self._albums[key].set_title(album)
-            self._albums[key].add_artist(artist)
-            self._albums[key].set_source(source.get_id())
-            self._albums[key].songs = []
-            self._add_item(
-                source, None, self._albums[key], 0, [self.model, 'album'])
-            self._add_item(
-                source, None, self._albums[key], 0, [self.model, 'artist'])
-
-        self._albums[key].songs.append(item)
-        self._add_item(source, None, item, 0, [self.model, 'song'])
-
-    @log
-    def _retrieval_finished(self, klass, model, _iter):
-        if not model[_iter][13]:
-            return
-
-        model[_iter][13] = klass.surface
-
-    @log
-    def _add_item(self, source, param, item, remaining=0, data=None):
-        if data is None:
-            return
-
-        model, category = data
-
-        self._items_found = (
-            self.model.iter_n_children(self._head_iters[0])
-            + self.model.iter_n_children(self._head_iters[1])
-            + self.model.iter_n_children(self._head_iters[2])
-            + self.model.iter_n_children(self._head_iters[3])
-        )
-
-        # We need to remember the view before the search view
-        emptysearchview = self._window.views[View.EMPTY]
-        if (self._window.curr_view != emptysearchview
-                and self._window.prev_view != emptysearchview):
-            self.previous_view = self._window.prev_view
-
-        if self._items_found == 0:
-            self.props.search_state = Search.State.NO_RESULT
-        else:
-            self.props.search_state = Search.State.RESULT
-
-        if remaining == 0:
-            self._window.notifications_popup.pop_loading()
-            self._view.show()
-
-        if not item or model != self.model:
-            return
-
-        self._offset += 1
-        title = utils.get_media_title(item)
-        item.set_title(title)
-        artist = utils.get_artist_name(item)
-
-        group = 3
-        try:
-            group = {'album': 0, 'artist': 1, 'song': 2}[category]
-        except KeyError:
-            pass
-
-        _iter = None
-        if category == 'album':
-            _iter = self.model.insert_with_values(
-                self._head_iters[group], -1, [0, 2, 3, 5, 9, 12],
-                [str(item.get_id()), title, artist, item, 2,
-                 category])
-        elif category == 'song':
-            # FIXME: source specific hack
-            if source.get_id() != 'grl-tracker-source':
-                fav = 2
-            else:
-                fav = item.get_favourite()
-            _iter = self.model.insert_with_values(
-                self._head_iters[group], -1, [0, 2, 3, 5, 9, 12],
-                [str(item.get_id()), title, artist, item, fav,
-                 category])
-        else:
-            if not artist.casefold() in self._artists:
-                _iter = self.model.insert_with_values(
-                    self._head_iters[group], -1, [0, 2, 5, 9, 12],
-                    [str(item.get_id()), artist, item, 2,
-                     category])
-                self._artists[artist.casefold()] = {
-                    'iter': _iter,
-                    'albums': []
-                }
-            self._artists[artist.casefold()]['albums'].append(item)
-
-        # FIXME: Figure out why iter can be None here, seems illogical.
-        if _iter is not None:
-            scale = self._view.get_scale_factor()
-            art = Art(Art.Size.SMALL, item, scale)
-            self.model[_iter][13] = art.surface
-            art.connect(
-                'finished', self._retrieval_finished, self.model, _iter)
-            art.lookup()
-
-        if self.model.iter_n_children(self._head_iters[group]) == 1:
-            path = self.model.get_path(self._head_iters[group])
-            path = self._filter_model.convert_child_path_to_path(path)
-            self._view.expand_row(path, False)
-
-    @log
-    def _add_list_renderers(self):
-        column = Gtk.TreeViewColumn()
-
-        # Add our own surface renderer, instead of the one provided by
-        # Gd. This avoids us having to set the model to a cairo.Surface
-        # which is currently not a working solution in pygobject.
-        # https://gitlab.gnome.org/GNOME/pygobject/issues/155
-        pixbuf_renderer = Gtk.CellRendererPixbuf(
-            xalign=0.5, yalign=0.5, xpad=12, ypad=2)
-        column.pack_start(pixbuf_renderer, False)
-        column.set_cell_data_func(
-            pixbuf_renderer, self._on_list_widget_pixbuf_renderer)
-        column.add_attribute(pixbuf_renderer, 'surface', 13)
-
-        # With the bugfix in libgd 9117650bda, the search results
-        # stopped aligning at the top. With the artists results not
-        # having a second line of text, this looks off.
-        # Revert to old behaviour by forcing the alignment to be top.
-        two_lines_renderer = Gd.TwoLinesRenderer(
-            wrap_mode=Pango.WrapMode.WORD_CHAR, xpad=12, xalign=0.0,
-            yalign=0, text_lines=2)
-        column.pack_start(two_lines_renderer, True)
-        column.set_cell_data_func(
-            two_lines_renderer, self._on_list_widget_two_lines_renderer)
-        column.add_attribute(two_lines_renderer, 'text', 2)
-        column.add_attribute(two_lines_renderer, 'line_two', 3)
-
-        title_renderer = Gtk.CellRendererText(
-            xpad=12, xalign=0.0, yalign=0.5, height=32,
-            ellipsize=Pango.EllipsizeMode.END, weight=Pango.Weight.BOLD)
-        column.pack_start(title_renderer, False)
-        column.set_cell_data_func(
-            title_renderer, self._on_list_widget_title_renderer)
-        column.add_attribute(title_renderer, 'text', 2)
-
-        self._star_handler.add_star_renderers(column)
-
-        selection_renderer = Gtk.CellRendererToggle(xpad=12, xalign=1.0)
-        column.pack_start(selection_renderer, False)
-        column.set_cell_data_func(
-            selection_renderer, self._on_list_widget_selection_renderer)
-        column.add_attribute(selection_renderer, 'active', 6)
-
-        self._view.append_column(column)
-
-    @log
-    def _is_header(self, model, iter_):
-        return model.iter_parent(iter_) is None
-
-    @log
-    def _on_list_widget_title_renderer(self, col, cell, model, iter_, data):
-        cell.props.visible = self._is_header(model, iter_)
-
-    @log
-    def _on_list_widget_pixbuf_renderer(self, col, cell, model, iter_, data):
-        if (not model[iter_][13]
-                or self._is_header(model, iter_)):
-            cell.props.visible = False
-            return
-
-        cell.props.surface = model[iter_][13]
-        cell.props.visible = True
-
-    @log
-    def _on_list_widget_two_lines_renderer(
-            self, col, cell, model, iter_, data):
-        if self._is_header(model, iter_):
-            cell.props.visible = False
-            return
-
-        cell.props.visible = True
-
-    @log
-    def _on_list_widget_selection_renderer(
-            self, col, cell, model, iter_, data):
-        if (self.props.selection_mode
-                and not self._is_header(model, iter_)):
-            cell.props.visible = True
-        else:
-            cell.props.visible = False
-
     @log
     def _populate(self, data=None):
         self._init = True
         self._headerbar.props.state = HeaderBar.State.MAIN
-
-    @log
-    def get_selected_songs(self, callback):
-        if self.get_visible_child() == self._album_widget:
-            callback(self._album_widget.get_selected_songs())
-        elif self.get_visible_child() == self._artist_albums_widget:
-            callback(self._artist_albums_widget.get_selected_songs())
-        else:
-            self._albums_index = 0
-            self._artists_albums_index = 0
-            self._items_selected = []
-            self._items_selected_callback = callback
-            self._get_selected_albums()
-
-    @log
-    def _get_selected_albums(self):
-        selected_iters = self._get_selected_iters()
-
-        self._albums_selected = [
-            self.model[iter_][5]
-            for iter_ in selected_iters
-            if self.model[iter_][12] == 'album']
-
-        if len(self._albums_selected):
-            self._get_selected_albums_songs()
-        else:
-            self._get_selected_artists()
-
-    @log
-    def _get_selected_albums_songs(self):
-        grilo.populate_album_songs(
-            self._albums_selected[self._albums_index],
-            self._add_selected_albums_songs)
-        self._albums_index += 1
-
-    @log
-    def _add_selected_albums_songs(
-            self, source, param, item, remaining=0, data=None):
-        if item:
-            self._items_selected.append(item)
-        if remaining == 0:
-            if self._albums_index < len(self._albums_selected):
-                self._get_selected_albums_songs()
-            else:
-                self._get_selected_artists()
-
-    @log
-    def _get_selected_artists(self):
-        selected_iters = self._get_selected_iters()
-
-        artists_selected = [
-            self._artists[self.model[iter_][2].casefold()]
-            for iter_ in selected_iters
-            if self.model[iter_][12] == 'artist']
-
-        self._artists_albums_selected = []
-        for artist in artists_selected:
-            self._artists_albums_selected.extend(artist['albums'])
-
-        if len(self._artists_albums_selected):
-            self._get_selected_artists_albums_songs()
-        else:
-            self._get_selected_songs()
-
-    @log
-    def _get_selected_artists_albums_songs(self):
-        grilo.populate_album_songs(
-            self._artists_albums_selected[self._artists_albums_index],
-            self._add_selected_artists_albums_songs)
-        self._artists_albums_index += 1
-
-    @log
-    def _add_selected_artists_albums_songs(
-            self, source, param, item, remaining=0, data=None):
-        if item:
-            self._items_selected.append(item)
-        if remaining == 0:
-            artist_albums = len(self._artists_albums_selected)
-            if self._artists_albums_index < artist_albums:
-                self._get_selected_artists_albums_songs()
-            else:
-                self._get_selected_songs()
-
-    @log
-    def _get_selected_songs(self):
-        selected_iters = self._get_selected_iters()
-        self._items_selected.extend([
-            self.model[iter_][5]
-            for iter_ in selected_iters
-            if self.model[iter_][12] == 'song'])
-        self._items_selected_callback(self._items_selected)
-
-    @log
-    def _filter_visible_func(self, model, _iter, data=None):
-        top_level = model.iter_parent(_iter) is None
-        visible = (not top_level or model.iter_has_child(_iter))
-
-        return visible
-
-    @log
-    def set_search_text(self, search_term, fields_filter):
-        query_matcher = {
-            'album': {
-                'search_all': Query.get_albums_with_any_match,
-                'search_artist': Query.get_albums_with_artist_match,
-                'search_composer': Query.get_albums_with_composer_match,
-                'search_album': Query.get_albums_with_album_match,
-                'search_track': Query.get_albums_with_track_match,
-            },
-            'artist': {
-                'search_all': Query.get_artists_with_any_match,
-                'search_artist': Query.get_artists_with_artist_match,
-                'search_composer': Query.get_artists_with_composer_match,
-                'search_album': Query.get_artists_with_album_match,
-                'search_track': Query.get_artists_with_track_match,
-            },
-            'song': {
-                'search_all': Query.get_songs_with_any_match,
-                'search_artist': Query.get_songs_with_artist_match,
-                'search_composer': Query.get_songs_with_composer_match,
-                'search_album': Query.get_songs_with_album_match,
-                'search_track': Query.get_songs_with_track_match,
-            },
-        }
-
-        self.model = Gtk.TreeStore(
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,    # item title or header text
-            GObject.TYPE_STRING,    # artist for albums and songs
-            GdkPixbuf.Pixbuf,       # Gd placeholder album art
-            GObject.TYPE_OBJECT,    # item
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT,
-            GObject.TYPE_STRING,
-            GObject.TYPE_INT,
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT,       # validation status
-            GObject.TYPE_STRING,    # type
-            object                  # album art surface
-        )
-
-        self._filter_model = self.model.filter_new(None)
-        self._filter_model.set_visible_func(self._filter_visible_func)
-        self._view.set_model(self._filter_model)
-
-        self._albums = {}
-        self._artists = {}
-
-        if search_term == "":
-            return
-
-        albums_iter = self.model.insert_with_values(
-            None, -1, [2, 9], [_("Albums"), 2])
-        artists_iter = self.model.insert_with_values(
-            None, -1, [2, 9], [_("Artists"), 2])
-        songs_iter = self.model.insert_with_values(
-            None, -1, [2, 9], [_("Songs"), 2])
-        playlists_iter = self.model.insert_with_values(
-            None, -1, [2, 9], [_("Playlists"), 2])
-
-        self._head_iters = [
-            albums_iter,
-            artists_iter,
-            songs_iter,
-            playlists_iter
-        ]
-
-        self._songs_model = self.model.filter_new(
-            self.model.get_path(songs_iter))
-
-        # Use queries for Tracker
-        if (not grilo.search_source
-                or grilo.search_source.get_id() == 'grl-tracker-source'):
-            for category in ('album', 'artist', 'song'):
-                query = query_matcher[category][fields_filter](search_term)
-                self._window.notifications_popup.push_loading()
-                grilo.populate_custom_query(
-                    query, self._add_item, -1, [self.model, category])
-        if (not grilo.search_source
-                or grilo.search_source.get_id() != 'grl-tracker-source'):
-            # nope, can't do - reverting to Search
-            grilo.search(search_term, self._add_search_item, self.model)
diff --git a/gnomemusic/views/songsview.py b/gnomemusic/views/songsview.py
index 73c5574e..5e6b9d5f 100644
--- a/gnomemusic/views/songsview.py
+++ b/gnomemusic/views/songsview.py
@@ -27,10 +27,9 @@ from gettext import gettext as _
 from gi.repository import Gdk, Gtk, Pango
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
-from gnomemusic.player import ValidationStatus, PlayerPlaylist
+from gnomemusic.coresong import CoreSong
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.views.baseview import BaseView
-import gnomemusic.utils as utils
 
 logger = logging.getLogger(__name__)
 
@@ -52,18 +51,24 @@ class SongsView(BaseView):
         :param GtkWidget window: The main window
         :param player: The main player object
         """
+        self._window = window
+        self._coremodel = self._window._app.props.coremodel
         super().__init__('songs', _("Songs"), window)
 
         self._offset = 0
         self._iter_to_clean = None
 
-        self._view.get_style_context().add_class('songs-list')
+        self._view.get_style_context().add_class('songs-list-old')
 
         self._add_list_renderers()
 
+        self._playlist_model = self._coremodel.props.playlist_sort
+
         self.player = player
         self.player.connect('song-changed', self._update_model)
-        self.player.connect('song-validated', self._on_song_validated)
+
+        self._model = self._view.props.model
+        self._view.show()
 
     @log
     def _setup_view(self):
@@ -73,7 +78,7 @@ class SongsView(BaseView):
         self._view = Gtk.TreeView()
         self._view.props.headers_visible = False
         self._view.props.valign = Gtk.Align.START
-        self._view.props.model = self.model
+        self._view.props.model = self._coremodel.props.songs_gtkliststore
         self._view.props.activate_on_single_click = True
 
         self._ctrl = Gtk.GestureMultiPress().new(self._view)
@@ -99,7 +104,7 @@ class SongsView(BaseView):
 
         selection_renderer = Gtk.CellRendererToggle()
         column_selection = Gtk.TreeViewColumn(
-            "Selected", selection_renderer, active=6)
+            "Selected", selection_renderer, active=1)
         column_selection.props.visible = False
         column_selection.props.fixed_width = 48
         self._view.append_column(column_selection)
@@ -111,17 +116,6 @@ class SongsView(BaseView):
         column_title.props.expand = True
         self._view.append_column(column_title)
 
-        column_star = Gtk.TreeViewColumn()
-        self._view.append_column(column_star)
-        self._star_handler.add_star_renderers(column_star)
-
-        duration_renderer = Gtk.CellRendererText(xpad=32, xalign=1.0)
-        column_duration = Gtk.TreeViewColumn()
-        column_duration.pack_start(duration_renderer, False)
-        column_duration.set_cell_data_func(
-            duration_renderer, self._on_list_widget_duration_render, None)
-        self._view.append_column(column_duration)
-
         artist_renderer = Gtk.CellRendererText(
             xpad=32, ellipsize=Pango.EllipsizeMode.END)
         column_artist = Gtk.TreeViewColumn("Artist", artist_renderer, text=3)
@@ -130,38 +124,29 @@ class SongsView(BaseView):
 
         album_renderer = Gtk.CellRendererText(
             xpad=32, ellipsize=Pango.EllipsizeMode.END)
-        column_album = Gtk.TreeViewColumn()
+        column_album = Gtk.TreeViewColumn("Album", album_renderer, text=4)
         column_album.props.expand = True
-        column_album.pack_start(album_renderer, True)
-        column_album.set_cell_data_func(
-            album_renderer, self._on_list_widget_album_render, None)
         self._view.append_column(column_album)
 
-    def _on_list_widget_duration_render(self, col, cell, model, itr, data):
-        item = model[itr][5]
-        if item:
-            seconds = item.get_duration()
-            track_time = utils.seconds_to_string(seconds)
-            cell.props.text = '{}'.format(track_time)
-
-    def _on_list_widget_album_render(self, coll, cell, model, _iter, data):
-        if not model.iter_is_valid(_iter):
-            return
+        duration_renderer = Gtk.CellRendererText(xalign=1.0)
+        column_duration = Gtk.TreeViewColumn(
+            "Duration", duration_renderer, text=5)
+        self._view.append_column(column_duration)
 
-        item = model[_iter][5]
-        if item:
-            cell.props.text = utils.get_album_title(item)
+        column_star = Gtk.TreeViewColumn()
+        self._view.append_column(column_star)
+        self._star_handler.add_star_renderers(column_star)
 
     def _on_list_widget_icon_render(self, col, cell, model, itr, data):
-        if not self.player.playing_playlist(PlayerPlaylist.Type.SONGS, None):
-            cell.props.visible = False
-            return False
-
         current_song = self.player.props.current_song
-        if model[itr][11] == ValidationStatus.FAILED:
+        if current_song is None:
+            return
+
+        coresong = model[itr][7]
+        if coresong.props.validation == CoreSong.Validation.FAILED:
             cell.props.icon_name = self._error_icon_name
             cell.props.visible = True
-        elif model[itr][5].get_id() == current_song.get_id():
+        elif coresong.props.grlid == current_song.props.grlid:
             cell.props.icon_name = self._now_playing_icon_name
             cell.props.visible = True
         else:
@@ -174,7 +159,7 @@ class SongsView(BaseView):
             self.model.clear()
             self._offset = 0
             self._populate()
-            grilo.changes_pending['Songs'] = False
+            # grilo.changes_pending['Songs'] = False
 
     @log
     def _on_selection_mode_changed(self, widget, data=None):
@@ -183,8 +168,7 @@ class SongsView(BaseView):
         cols = self._view.get_columns()
         cols[1].props.visible = self.props.selection_mode
 
-        if (not self.props.selection_mode
-                and grilo.changes_pending['Songs']):
+        if not self.props.selection_mode:
             self._on_changes_pending()
 
     @log
@@ -205,10 +189,12 @@ class SongsView(BaseView):
         if self.props.selection_mode:
             return
 
-        itr = self.model.get_iter(path)
-        self.player.set_playlist(
-            PlayerPlaylist.Type.SONGS, None, self.model, itr)
-        self.player.play()
+        itr = self._view.props.model.get_iter(path)
+        coresong = self._view.props.model[itr][7]
+        self._window._app._coremodel.set_playlist_model(
+            PlayerPlaylist.Type.SONGS, self._view.props.model)
+
+        self.player.play(coresong)
 
     @log
     def _on_view_clicked(self, gesture, n_press, x, y):
@@ -223,10 +209,9 @@ class SongsView(BaseView):
         # activation.
         if self.props.selection_mode:
             path, col, cell_x, cell_y = self._view.get_path_at_pos(x, y)
-            iter_ = self.model.get_iter(path)
-            self.model[iter_][6] = not self.model[iter_][6]
-
-            self.props.selected_items_count = len(self.get_selected_songs())
+            iter_ = self._view.props.model.get_iter(path)
+            self._model[iter_][1] = not self._model[iter_][1]
+            self._model[iter_][7].props.selected = self._model[iter_][7]
 
     @log
     def _update_model(self, player):
@@ -234,64 +219,43 @@ class SongsView(BaseView):
 
         :param Player player: The main player object
         """
+        # iter_to_clean is necessary because of a bug in GtkTreeView
+        # See https://gitlab.gnome.org/GNOME/gtk/issues/503
         if self._iter_to_clean:
-            self.model[self._iter_to_clean][10] = False
-        if not player.playing_playlist(PlayerPlaylist.Type.SONGS, None):
-            return False
-
-        index = self.player.props.current_song_index
-        iter_ = self.model.get_iter_from_string(str(index))
-        self.model[iter_][10] = True
-        path = self.model.get_path(iter_)
-        self._view.scroll_to_cell(path, None, False, 0., 0.)
-        if self.model[iter_][8] != self._error_icon_name:
-            self._iter_to_clean = iter_.copy()
-        return False
+            self._view.props.model[self._iter_to_clean][9] = False
 
-    @log
-    def _on_song_validated(self, player, index, status):
-        if not player.playing_playlist(PlayerPlaylist.Type.SONGS, None):
-            return
+        index = self.player.props.position
+        current_coresong = self._playlist_model[index]
+        for idx, liststore in enumerate(self._view.props.model):
+            if liststore[7] == current_coresong:
+                break
 
-        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:
-            self._view.set_model(self.model)
-            self._window.notifications_popup.pop_loading()
-            self._view.show()
-            return
+        iter_ = self._view.props.model.get_iter_from_string(str(idx))
+        path = self._view.props.model.get_path(iter_)
+        self._view.props.model[iter_][9] = True
+        self._view.scroll_to_cell(path, None, True, 0.5, 0.5)
 
-        self._offset += 1
-        item.set_title(utils.get_media_title(item))
-        artist = utils.get_artist_name(item)
-
-        if not item.get_url():
-            return
+        if self._view.props.model[iter_][0] != self._error_icon_name:
+            self._iter_to_clean = iter_.copy()
 
-        self.model.insert_with_valuesv(
-            -1, [2, 3, 5, 9],
-            [utils.get_media_title(item), artist, item, item.get_favourite()])
+        return False
 
     @log
     def _populate(self, data=None):
         """Populates the view"""
         self._init = True
-        if grilo.tracker:
-            self._window.notifications_popup.push_loading()
-            grilo.populate_songs(self._offset, self._add_item)
 
-    @log
-    def get_selected_songs(self, callback=None):
-        """Returns a list of selected songs
+    def _select(self, value):
+        with self._model.freeze_notify():
+            itr = self._model.iter_children(None)
+            while itr is not None:
+                self._model[itr][7].props.selected = value
+                self._model[itr][1] = value
 
-        In this view this will be the all the songs selected
-        :returns: All selected songs
-        :rtype: A list of songs
-        """
-        selected_songs = [row[5] for row in self.model if row[6]]
-        if not callback:
-            return selected_songs
-        callback(selected_songs)
+                itr = self._model.iter_next(itr)
+
+    def select_all(self):
+        self._select(True)
+
+    def unselect_all(self):
+        self._select(False)
diff --git a/gnomemusic/widgets/albumcover.py b/gnomemusic/widgets/albumcover.py
index f76ecb65..5dc55a40 100644
--- a/gnomemusic/widgets/albumcover.py
+++ b/gnomemusic/widgets/albumcover.py
@@ -24,11 +24,11 @@
 
 import gi
 gi.require_version('Grl', '0.3')
-from gi.repository import Gdk, GLib, GObject, Grl, Gtk
+from gi.repository import Gdk, GLib, GObject, Gtk
 
 from gnomemusic import log
-from gnomemusic import utils
 from gnomemusic.albumartcache import Art
+from gnomemusic.corealbum import CoreAlbum
 from gnomemusic.widgets.twolinetip import TwoLineTip
 
 
@@ -58,7 +58,7 @@ class AlbumCover(Gtk.FlowBoxChild):
         return '<AlbumCover>'
 
     @log
-    def __init__(self, media):
+    def __init__(self, corealbum):
         """Initialize the AlbumCover
 
         :param Grl.Media media: The media object to use
@@ -67,15 +67,15 @@ class AlbumCover(Gtk.FlowBoxChild):
 
         AlbumCover._nr_albums += 1
 
-        self._media = media
+        self._corealbum = corealbum
 
         self._tooltip = TwoLineTip()
 
-        artist = utils.get_artist_name(media)
-        title = utils.get_media_title(media)
+        artist = self._corealbum.props.artist
+        title = self._corealbum.props.title
 
-        self._tooltip.props.title = utils.get_artist_name(media)
-        self._tooltip.props.subtitle = utils.get_media_title(media)
+        self._tooltip.props.title = artist
+        self._tooltip.props.subtitle = title
 
         self._artist_label.props.label = artist
         self._title_label.props.label = title
@@ -102,17 +102,17 @@ class AlbumCover(Gtk.FlowBoxChild):
         # reasonably responsive view while loading the actual
         # covers.
         GLib.timeout_add(
-            50 * self._nr_albums, self._cover_stack.update, media,
+            50 * self._nr_albums, self._cover_stack.update, self._corealbum,
             priority=GLib.PRIORITY_LOW)
 
-    @GObject.Property(type=Grl.Media, flags=GObject.ParamFlags.READABLE)
-    def media(self):
-        """Media item used in AlbumCover
+    @GObject.Property(type=CoreAlbum, flags=GObject.ParamFlags.READABLE)
+    def corealbum(self):
+        """CoreAlbum object used in AlbumCover
 
-        :returns: The media used
-        :rtype: Grl.Media
+        :returns: The album used
+        :rtype: CoreAlbum
         """
-        return self._media
+        return self._corealbum
 
     @Gtk.Template.Callback()
     @log
diff --git a/gnomemusic/widgets/albumwidget.py b/gnomemusic/widgets/albumwidget.py
index 1eea57f0..dba32f17 100644
--- a/gnomemusic/widgets/albumwidget.py
+++ b/gnomemusic/widgets/albumwidget.py
@@ -1,38 +1,11 @@
-# Copyright (c) 2016 The GNOME Music Developers
-#
-# GNOME Music is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# GNOME Music is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-#
-# The GNOME Music authors hereby grant permission for non-GPL compatible
-# GStreamer plugins to be used and distributed together with GStreamer
-# and GNOME Music.  This permission is above and beyond the permissions
-# granted by the GPL license by which GNOME Music is covered.  If you
-# modify this code, you may extend this exception to your version of the
-# 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 gettext import ngettext
-from gi.repository import GdkPixbuf, GObject, Grl, Gtk
+from gi.repository import GObject, Grl, 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
 from gnomemusic.widgets.disclistboxwidget import DiscListBox  # noqa: F401
-from gnomemusic.widgets.songwidget import SongWidget
-import gnomemusic.utils as utils
 
 
 @Gtk.Template(resource_path='/org/gnome/Music/ui/AlbumWidget.ui')
@@ -49,7 +22,7 @@ class AlbumWidget(Gtk.EventBox):
     _composer_label = Gtk.Template.Child()
     _composer_info_label = Gtk.Template.Child()
     _cover_stack = Gtk.Template.Child()
-    _disc_listbox = Gtk.Template.Child()
+    _listbox = Gtk.Template.Child()
     _released_info_label = Gtk.Template.Child()
     _running_info_label = Gtk.Template.Child()
     _title_label = Gtk.Template.Child()
@@ -63,63 +36,39 @@ class AlbumWidget(Gtk.EventBox):
         return '<AlbumWidget>'
 
     @log
-    def __init__(self, player):
+    def __init__(self, player, parent_view):
         """Initialize the AlbumWidget.
 
         :param player: The player object
+        :param parent_view: The view this widget is part of
         """
         super().__init__()
 
         self._album = None
-        self._songs = []
+        self._album_model = None
+        self._signal_id = None
 
         self._cover_stack.props.size = Art.Size.LARGE
+        self._parent_view = parent_view
         self._player = player
-        self._iter_to_clean = None
 
-        self._create_model()
         self._album_name = None
 
-        self.bind_property(
-            'selection-mode', self._disc_listbox, 'selection-mode',
-            GObject.BindingFlags.BIDIRECTIONAL)
-
     @log
-    def _create_model(self):
-        """Create the ListStore model for this widget."""
-        self._model = Gtk.ListStore(
-            GObject.TYPE_STRING,  # title
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GdkPixbuf.Pixbuf,    # icon
-            GObject.TYPE_OBJECT,  # song object
-            GObject.TYPE_BOOLEAN,  # item selected
-            GObject.TYPE_STRING,
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT,  # icon shown
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT
-        )
-
-    @log
-    def update(self, album):
+    def update(self, corealbum):
         """Update the album widget.
 
-        :param Grl.Media album: The grilo media album
+        :param CoreAlbum album: The CoreAlbum object
         """
-        # reset view
-        self._songs = []
-        self._create_model()
-        for widget in self._disc_listbox.get_children():
-            self._disc_listbox.remove(widget)
+        if self._signal_id:
+            self._album_model.disconnect(self._signal_id)
 
-        self._cover_stack.update(album)
+        self._cover_stack.update(corealbum)
 
         self._duration = 0
 
-        self._album_name = utils.get_album_title(album)
-        artist = utils.get_artist_name(album)
+        self._album_name = corealbum.props.title
+        artist = corealbum.props.artist
 
         self._title_label.props.label = self._album_name
         self._title_label.props.tooltip_text = self._album_name
@@ -127,22 +76,58 @@ class AlbumWidget(Gtk.EventBox):
         self._artist_label.props.label = artist
         self._artist_label.props.tooltip_text = artist
 
-        year = utils.get_media_year(album)
-        if not year:
-            year = '----'
-        self._released_info_label.props.label = year
+        self._released_info_label.props.label = corealbum.props.year
+
+        self._set_composer_label(corealbum)
 
-        self._set_composer_label(album)
+        self._album = corealbum.props.media
+        self._album_model = corealbum.props.model
+        self._signal_id = self._album_model.connect_after(
+            "items-changed", self._on_model_items_changed)
+        self._listbox.bind_model(self._album_model, self._create_widget)
 
-        self._album = album
+        corealbum.connect("notify::duration", self._on_duration_changed)
 
-        self._player.connect('song-changed', self._update_model)
+        self._album_model.items_changed(0, 0, 0)
 
-        grilo.populate_album_songs(album, self.add_item)
+    def _create_widget(self, disc):
+        disc_box = self._create_disc_box(
+            disc.props.disc_nr, disc.model)
+
+        self.bind_property(
+            "selection-mode", disc_box, "selection-mode",
+            GObject.BindingFlags.BIDIRECTIONAL
+            | GObject.BindingFlags.SYNC_CREATE)
+
+        return disc_box
+
+    def _create_disc_box(self, disc_nr, album_model):
+        disc_box = DiscBox(album_model)
+        disc_box.set_disc_number(disc_nr)
+        disc_box.props.show_durations = False
+        disc_box.props.show_favorites = False
+        disc_box.props.show_song_numbers = True
+        disc_box.connect('song-activated', self._song_activated)
+
+        return disc_box
+
+    def _on_model_items_changed(self, model, position, removed, added):
+        n_items = model.get_n_items()
+        if n_items == 1:
+            row = self._listbox.get_row_at_index(0)
+            row.props.selectable = False
+            discbox = row.get_child()
+            discbox.props.show_disc_label = False
+        else:
+            for i in range(n_items):
+                row = self._listbox.get_row_at_index(i)
+                row.props.selectable = False
+                discbox = row.get_child()
+                discbox.props.show_disc_label = True
 
     @log
-    def _set_composer_label(self, album):
-        composer = album.get_composer()
+    def _set_composer_label(self, corealbum):
+        composer = corealbum.props.composer
         show = False
 
         if composer:
@@ -154,30 +139,19 @@ class AlbumWidget(Gtk.EventBox):
         self._composer_label.props.visible = show
         self._composer_info_label.props.visible = show
 
-    @log
-    def _set_duration_label(self):
-        mins = (self._duration // 60) + 1
+    def _on_duration_changed(self, coredisc, duration):
+        mins = (coredisc.props.duration // 60) + 1
         self._running_info_label.props.label = ngettext(
             "{} minute", "{} minutes", mins).format(mins)
 
-    @Gtk.Template.Callback()
     @log
-    def _on_selection_changed(self, widget):
-        n_items = len(self._disc_listbox.get_selected_items())
-        self.props.selected_items_count = n_items
+    def _on_selection_changed(self, klass, value):
+        n_items = 0
+        for song in self._model[0]:
+            if song.props.selected:
+                n_items += 1
 
-    @log
-    def _create_disc_box(self, disc_nr, disc_songs):
-        disc_box = DiscBox(self._model)
-        disc_box.set_songs(disc_songs)
-        disc_box.set_disc_number(disc_nr)
-        disc_box.props.columns = 1
-        disc_box.props.show_durations = True
-        disc_box.props.show_favorites = True
-        disc_box.props.show_song_numbers = False
-        disc_box.connect('song-activated', self._song_activated)
-
-        return disc_box
+        self.props.selected_items_count = n_items
 
     @log
     def _song_activated(self, widget, song_widget):
@@ -185,84 +159,26 @@ class AlbumWidget(Gtk.EventBox):
             song_widget.props.selected = not song_widget.props.selected
             return
 
-        self._player.set_playlist(
-            PlayerPlaylist.Type.ALBUM, self._album_name, song_widget.model,
-            song_widget.itr)
-        self._player.play()
-        return True
-
-    @log
-    def add_item(self, source, prefs, song, remaining, data=None):
-        """Add a song to the item to album list.
-
-        If no song is remaining create DiscBox and display the widget.
-        :param GrlTrackerSource source: The grilo source
-        :param prefs: not used
-        :param GrlMedia song: The grilo media object
-        :param int remaining: Remaining number of items to add
-        :param data: User data
-        """
-        if song:
-            self._songs.append(song)
-            self._duration += song.get_duration()
-            return
-
-        if remaining == 0:
-            discs = {}
-            for song in self._songs:
-                disc_nr = song.get_album_disc_number()
-                if disc_nr not in discs.keys():
-                    discs[disc_nr] = [song]
-                else:
-                    discs[disc_nr].append(song)
-
-            for disc_nr in discs:
-                disc = self._create_disc_box(disc_nr, discs[disc_nr])
-                if len(discs) == 1:
-                    disc.props.show_disc_label = False
-                self._disc_listbox.add(disc)
+        signal_id = None
+        coremodel = self._parent_view._window._app.props.coremodel
 
-            self._set_duration_label()
-            self._update_model(self._player)
+        def _on_playlist_loaded(klass):
+            self._player.play(song_widget.props.coresong)
+            coremodel.disconnect(signal_id)
 
-    @log
-    def _update_model(self, player):
-        """Updates model when the song changes
-
-        :param Player player: The main player object
-        """
-        if not player.playing_playlist(
-                PlayerPlaylist.Type.ALBUM, self._album_name):
-            return True
-
-        current_song = player.props.current_song
-        self._duration = 0
-        song_passed = False
-
-        for song in self._songs:
-            song_widget = song.song_widget
-            self._duration += song.get_duration()
-
-            if (song.get_id() == current_song.get_id()):
-                song_widget.props.state = SongWidget.State.PLAYING
-                song_passed = True
-            elif (song_passed):
-                # Counter intuitive, but this is due to call order.
-                song_widget.props.state = SongWidget.State.UNPLAYED
-            else:
-                song_widget.props.state = SongWidget.State.PLAYED
-
-        self._set_duration_label()
+        signal_id = coremodel.connect("playlist-loaded", _on_playlist_loaded)
+        coremodel.set_playlist_model(
+            PlayerPlaylist.Type.ALBUM, self._album_model)
 
         return True
 
     @log
     def select_all(self):
-        self._disc_listbox.select_all()
+        self._listbox.select_all()
 
     @log
     def select_none(self):
-        self._disc_listbox.select_none()
+        self._listbox.select_none()
 
     @log
     def get_selected_songs(self):
@@ -271,7 +187,13 @@ class AlbumWidget(Gtk.EventBox):
         :returns: selected songs
         :rtype: list
         """
-        return self._disc_listbox.get_selected_items()
+        selected_songs = []
+
+        for song in self._model:
+            if song.props.selected:
+                selected_songs.append(song.props.media)
+
+        return selected_songs
 
     @GObject.Property(
         type=Grl.Media, default=False, flags=GObject.ParamFlags.READABLE)
diff --git a/gnomemusic/widgets/artistalbumswidget.py b/gnomemusic/widgets/artistalbumswidget.py
index aa132db7..354c62e9 100644
--- a/gnomemusic/widgets/artistalbumswidget.py
+++ b/gnomemusic/widgets/artistalbumswidget.py
@@ -29,13 +29,11 @@ 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
 
 logger = logging.getLogger(__name__)
 
 
-@Gtk.Template(resource_path='/org/gnome/Music/ui/ArtistAlbumsWidget.ui')
-class ArtistAlbumsWidget(Gtk.Box):
+class ArtistAlbumsWidget(Gtk.ListBox):
     """Widget containing all albums by an artist
 
     A vertical list of ArtistAlbumWidget, containing all the albums
@@ -45,153 +43,83 @@ class ArtistAlbumsWidget(Gtk.Box):
 
     __gtype_name__ = 'ArtistAlbumsWidget'
 
-    _artist_label = Gtk.Template.Child()
-
     selected_items_count = GObject.Property(type=int, default=0, minimum=0)
     selection_mode = GObject.Property(type=bool, default=False)
 
+    __gsignals__ = {
+        "ready": (GObject.SignalFlags.RUN_FIRST, None, ()),
+    }
+
     def __repr__(self):
         return '<ArtistAlbumsWidget>'
 
     @log
     def __init__(
-            self, artist, albums, player, window,
-            selection_mode_allowed=False):
-        super().__init__(orientation=Gtk.Orientation.VERTICAL)
+            self, coreartist, player, window, selection_mode_allowed=False):
+        super().__init__()
+        self._artist = coreartist.props.artist
+        self._model = coreartist.props.model
         self._player = player
-        self._artist = artist
-        self._window = window
         self._selection_mode_allowed = selection_mode_allowed
-
-        self._artist_label.props.label = self._artist
+        self._window = window
 
         self._widgets = []
 
-        self._create_model()
-
-        self._model.connect('row-changed', self._model_row_changed)
-
-        hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
-        self._album_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
-                                  spacing=48)
-        hbox.pack_start(self._album_box, False, False, 16)
-
-        self._scrolled_window = Gtk.ScrolledWindow()
-        self._scrolled_window.set_policy(Gtk.PolicyType.NEVER,
-                                         Gtk.PolicyType.AUTOMATIC)
-        self._scrolled_window.add(hbox)
-        self.pack_start(self._scrolled_window, True, True, 0)
-
         self._cover_size_group = Gtk.SizeGroup.new(
             Gtk.SizeGroupMode.HORIZONTAL)
         self._songs_grid_size_group = Gtk.SizeGroup.new(
             Gtk.SizeGroupMode.HORIZONTAL)
 
-        self._window.notifications_popup.push_loading()
+        self._nb_albums_loaded = 0
+        self._model.props.model.connect_after(
+            "items-changed", self. _on_model_items_changed)
+        self.bind_model(self._model, self._add_album)
 
-        self._albums_to_load = len(albums)
-        for album in albums:
-            self._add_album(album)
+        self.get_style_context().add_class("artist-albums-widget")
+        self.show_all()
 
-        self._player.connect('song-changed', self._update_model)
+    def _song_activated(self, widget, song_widget):
+        self._album = None
 
-    @log
-    def _create_model(self):
-        """Create the ListStore model for this widget."""
-        self._model = Gtk.ListStore(
-            GObject.TYPE_STRING,  # title
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,
-            GObject.TYPE_STRING,    # placeholder
-            GObject.TYPE_OBJECT,  # song object
-            GObject.TYPE_BOOLEAN,  # item selected
-            GObject.TYPE_STRING,
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT,  # icon shown
-            GObject.TYPE_BOOLEAN,
-            GObject.TYPE_INT
-        )
+        if self.props.selection_mode:
+            return
 
-    @log
-    def _on_album_displayed(self, data=None):
-        self._albums_to_load -= 1
-        if self._albums_to_load == 0:
-            self._window.notifications_popup.pop_loading()
-            self.show_all()
+        coremodel = self._player._app.props.coremodel
 
-    @log
-    def _add_album(self, album):
+        def _on_playlist_loaded(artistalbumwidget):
+            self._player.play(song_widget.props.coresong)
+            coremodel.disconnect(signal_id)
+
+        signal_id = coremodel.connect("playlist-loaded", _on_playlist_loaded)
+        coremodel.set_playlist_model(PlayerPlaylist.Type.ARTIST, self._model)
+
+    def _add_album(self, corealbum):
         widget = ArtistAlbumWidget(
-            album, self._player, self._model, self._selection_mode_allowed,
-            self._songs_grid_size_group, self._cover_size_group)
+            corealbum, self._selection_mode_allowed,
+            self._songs_grid_size_group, self._cover_size_group, self._window)
 
         self.bind_property(
             'selection-mode', widget, 'selection-mode',
             GObject.BindingFlags.BIDIRECTIONAL
             | GObject.BindingFlags.SYNC_CREATE)
 
-        self._album_box.pack_start(widget, False, False, 0)
         self._widgets.append(widget)
-
-        widget.connect('songs-loaded', self._on_album_displayed)
-
-    @log
-    def _update_model(self, player):
-        """Updates model when the song changes
-
-        :param Player player: The main player object
-        """
-        if not player.playing_playlist(
-                PlayerPlaylist.Type.ARTIST, self._artist):
-            self._clean_model()
-            return False
-
-        current_song = player.props.current_song
-        song_passed = False
-        itr = self._model.get_iter_first()
-
-        while itr:
-            song = self._model[itr][5]
-            song_widget = song.song_widget
-
-            if (song.get_id() == current_song.get_id()):
-                song_widget.props.state = SongWidget.State.PLAYING
-                song_passed = True
-            elif (song_passed):
-                # Counter intuitive, but this is due to call order.
-                song_widget.props.state = SongWidget.State.UNPLAYED
-            else:
-                song_widget.props.state = SongWidget.State.PLAYED
-
-            itr = self._model.iter_next(itr)
-
-        return False
-
-    @log
-    def _clean_model(self):
-        itr = self._model.get_iter_first()
-
-        while itr:
-            song = self._model[itr][5]
-            song_widget = song.song_widget
-            song_widget.props.state = SongWidget.State.UNPLAYED
-
-            itr = self._model.iter_next(itr)
-
-        return False
-
-    @log
-    def _model_row_changed(self, model, path, itr):
-        if not self.props.selection_mode:
-            return
-
-        selected_items = 0
-        for row in model:
-            if row[6]:
-                selected_items += 1
-
-        self.props.selected_items_count = selected_items
+        widget.connect("ready", self._on_album_ready)
+        widget.connect("song-activated", self._song_activated)
+
+        return widget
+
+    def _on_album_ready(self, artistalbumwidget):
+        self._nb_albums_loaded += 1
+        if self._nb_albums_loaded == self._model.get_n_items():
+            artistalbumwidget.disconnect_by_func(self._on_album_ready)
+            self._nb_albums_loaded = 0
+            self.emit("ready")
+
+    def _on_model_items_changed(self, model, position, removed, added):
+        for i in range(model.get_n_items()):
+            row = self.get_row_at_index(i)
+            row.props.selectable = False
 
     @log
     def select_all(self):
diff --git a/gnomemusic/widgets/artistalbumwidget.py b/gnomemusic/widgets/artistalbumwidget.py
index 8d8fda88..8b81fa78 100644
--- a/gnomemusic/widgets/artistalbumwidget.py
+++ b/gnomemusic/widgets/artistalbumwidget.py
@@ -26,10 +26,8 @@ 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
+from gnomemusic.widgets.songwidget import SongWidget
 
 
 @Gtk.Template(resource_path='/org/gnome/Music/ui/ArtistAlbumWidget.ui')
@@ -40,39 +38,33 @@ class ArtistAlbumWidget(Gtk.Box):
     _album_box = Gtk.Template.Child()
     _cover_stack = Gtk.Template.Child()
     _disc_list_box = Gtk.Template.Child()
-    _title = Gtk.Template.Child()
-    _year = Gtk.Template.Child()
+    _title_year = Gtk.Template.Child()
 
     selection_mode = GObject.Property(type=bool, default=False)
 
     __gsignals__ = {
-        'songs-loaded': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "ready": (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "song-activated": (
+            GObject.SignalFlags.RUN_FIRST, None, (SongWidget, )
+        ),
     }
 
     def __repr__(self):
         return '<ArtistAlbumWidget>'
 
-    @log
     def __init__(
-            self, media, player, model, selection_mode_allowed,
-            size_group=None, cover_size_group=None):
+            self, corealbum, selection_mode_allowed, size_group=None,
+            cover_size_group=None, window=None):
         super().__init__(orientation=Gtk.Orientation.HORIZONTAL)
 
         self._size_group = size_group
         self._cover_size_group = cover_size_group
 
-        self._media = media
-        self._player = player
-        self._artist = utils.get_artist_name(self._media)
-        self._album_title = utils.get_album_title(self._media)
-        self._model = model
         self._selection_mode = False
         self._selection_mode_allowed = selection_mode_allowed
 
-        self._songs = []
-
         self._cover_stack.props.size = Art.Size.MEDIUM
-        self._cover_stack.update(self._media)
+        self._cover_stack.update(corealbum)
 
         allowed = self._selection_mode_allowed
         self._disc_list_box.props.selection_mode_allowed = allowed
@@ -82,11 +74,10 @@ class ArtistAlbumWidget(Gtk.Box):
             GObject.BindingFlags.BIDIRECTIONAL
             | GObject.BindingFlags.SYNC_CREATE)
 
-        self._title.props.label = self._album_title
-        year = utils.get_media_year(self._media)
-
-        if year:
-            self._year.props.label = year
+        self._title_year.props.label = corealbum.props.title
+        year = corealbum.props.year
+        if year != "----":
+            self._title_year.props.label += " ({})".format(year)
 
         if self._size_group:
             self._size_group.add_widget(self._album_box)
@@ -94,55 +85,55 @@ class ArtistAlbumWidget(Gtk.Box):
         if self._cover_size_group:
             self._cover_size_group.add_widget(self._cover_stack)
 
-        grilo.populate_album_songs(self._media, self._add_item)
+        self._nb_disc_box_loaded = 0
+        self._model = corealbum.props.model
+        self._model.props.model.connect_after(
+            "items-changed", self._on_model_items_changed)
+        self._disc_list_box.bind_model(
+            self._model, self._create_widget)
 
-    @log
-    def _create_disc_box(self, disc_nr, disc_songs):
-        disc_box = DiscBox(self._model)
-        disc_box.set_songs(disc_songs)
+    def _create_widget(self, disc):
+        disc_box = self._create_disc_box(disc.props.disc_nr, disc.model)
+
+        return disc_box
+
+    def _create_disc_box(self, disc_nr, album_model):
+        disc_box = DiscBox(album_model)
         disc_box.set_disc_number(disc_nr)
-        disc_box.props.columns = 2
         disc_box.props.show_durations = False
         disc_box.props.show_favorites = False
         disc_box.props.show_song_numbers = True
+        disc_box.connect("ready", self._on_discbox_ready)
         disc_box.connect('song-activated', self._song_activated)
 
         return disc_box
 
-    @log
-    def _add_item(self, source, prefs, song, remaining, data=None):
-        if song:
-            self._songs.append(song)
-            return
-
-        discs = {}
-        for song in self._songs:
-            disc_nr = song.get_album_disc_number()
-            if disc_nr not in discs.keys():
-                discs[disc_nr] = [song]
-            else:
-                discs[disc_nr].append(song)
-
-        for disc_nr in discs:
-            disc = self._create_disc_box(disc_nr, discs[disc_nr])
-            self._disc_list_box.add(disc)
-            if len(discs) == 1:
-                disc.props.show_disc_label = False
+    def _on_discbox_ready(self, klass):
+        self._nb_disc_box_loaded += 1
+        if self._nb_disc_box_loaded == self._model.get_n_items():
+            klass.disconnect_by_func(self._on_discbox_ready)
+            self._nb_disc_box_loaded = 0
+            self.emit("ready")
+
+    def _on_model_items_changed(self, model, position, removed, added):
+        n_items = model.get_n_items()
+        if n_items == 1:
+            row = self._disc_list_box.get_row_at_index(0)
+            row.props.selectable = False
+            discbox = row.get_child()
+            discbox.props.show_disc_label = False
+        else:
+            for i in range(n_items):
+                row = self._disc_list_box.get_row_at_index(i)
+                row.props.selectable = False
+                discbox = row.get_child()
+                discbox.props.show_disc_label = True
 
-        if remaining == 0:
-            self.emit("songs-loaded")
-
-    @log
     def _song_activated(self, widget, song_widget):
         if self.props.selection_mode:
             return
 
-        self._player.set_playlist(
-            PlayerPlaylist.Type.ARTIST, self._artist, song_widget.model,
-            song_widget.itr)
-        self._player.play()
-
-        return True
+        self.emit("song-activated", song_widget)
 
     @log
     def select_all(self):
diff --git a/gnomemusic/widgets/sidebarrow.py b/gnomemusic/widgets/artisttile.py
similarity index 78%
rename from gnomemusic/widgets/sidebarrow.py
rename to gnomemusic/widgets/artisttile.py
index 798faaf2..c762b1fd 100644
--- a/gnomemusic/widgets/sidebarrow.py
+++ b/gnomemusic/widgets/artisttile.py
@@ -1,4 +1,4 @@
-# Copyright 2018 The GNOME Music developers
+# Copyright 2019 The GNOME Music developers
 #
 # GNOME Music is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -25,35 +25,43 @@
 from gi.repository import GObject, Gtk
 
 from gnomemusic import log
+from gnomemusic.coreartist import CoreArtist
 
 
-@Gtk.Template(resource_path='/org/gnome/Music/ui/SidebarRow.ui')
-class SidebarRow(Gtk.ListBoxRow):
+@Gtk.Template(resource_path='/org/gnome/Music/ui/ArtistTile.ui')
+class ArtistTile(Gtk.EventBox):
     """Row for sidebars
 
     Contains a label and an optional checkbox.
     """
 
-    __gtype_name__ = 'SidebarRow'
+    __gtype_name__ = 'ArtistTile'
 
     _check = Gtk.Template.Child()
     _label = Gtk.Template.Child()
     _revealer = Gtk.Template.Child()
 
+    coreartist = GObject.Property(type=CoreArtist, default=None)
     selected = GObject.Property(type=bool, default=False)
     selection_mode = GObject.Property(type=bool, default=False)
     text = GObject.Property(type=str, default='')
 
     def __repr__(self):
-        return '<SidebarRow>'
+        return '<ArtistTile>'
 
     @log
-    def __init__(self):
+    def __init__(self, coreartist=None):
         super().__init__()
 
+        self.props.coreartist = coreartist
+
         self.bind_property(
             'selected', self._check, 'active',
             GObject.BindingFlags.BIDIRECTIONAL)
+        if coreartist:
+            self.bind_property(
+                "selected", coreartist, "selected",
+                GObject.BindingFlags.BIDIRECTIONAL)
         self.bind_property('selection-mode', self._revealer, 'reveal-child')
         self.bind_property('text', self._label, 'label')
         self.bind_property('text', self._label, 'tooltip-text')
diff --git a/gnomemusic/widgets/coverstack.py b/gnomemusic/widgets/coverstack.py
index 4a981193..ae9eff81 100644
--- a/gnomemusic/widgets/coverstack.py
+++ b/gnomemusic/widgets/coverstack.py
@@ -96,11 +96,11 @@ class CoverStack(Gtk.Stack):
         self._loading_cover.props.surface = icon
 
     @log
-    def update(self, media):
-        """Update the stack with the given media
+    def update(self, coresong):
+        """Update the stack with the given CoreSong
 
-        Update the stack with the art retrieved from the given media.
-        :param Grl.Media media: The media object
+        Update the stack with the art retrieved from the given Coresong.
+        :param CoreSong coresong: The CoreSong object
         """
         if self._handler_id and self._art:
             # Remove a possible dangling 'finished' callback if update
@@ -113,7 +113,7 @@ class CoverStack(Gtk.Stack):
 
         self._active_child = self.props.visible_child_name
 
-        self._art = Art(self.props.size, media, self.props.scale_factor)
+        self._art = Art(self.props.size, coresong, self.props.scale_factor)
         self._handler_id = self._art.connect('finished', self._art_retrieved)
         self._art.lookup()
 
diff --git a/gnomemusic/widgets/disclistboxwidget.py b/gnomemusic/widgets/disclistboxwidget.py
index 3faa32c7..957538d8 100644
--- a/gnomemusic/widgets/disclistboxwidget.py
+++ b/gnomemusic/widgets/disclistboxwidget.py
@@ -27,10 +27,9 @@ from gi.repository import Gdk, GObject, Gtk
 
 from gnomemusic import log
 from gnomemusic.widgets.songwidget import SongWidget
-import gnomemusic.utils as utils
 
 
-class DiscSongsFlowBox(Gtk.FlowBox):
+class DiscSongsFlowBox(Gtk.ListBox):
     """FlowBox containing the songs on one disc
 
     DiscSongsFlowBox allows setting the number of columns to
@@ -42,45 +41,11 @@ class DiscSongsFlowBox(Gtk.FlowBox):
         return '<DiscSongsFlowBox>'
 
     @log
-    def __init__(self, columns=1):
+    def __init__(self):
         """Initialize
-
-        :param int columns: The number of columns the widget uses
         """
         super().__init__(selection_mode=Gtk.SelectionMode.NONE)
 
-        self._columns = 1
-        self.props.columns = columns
-
-        self.get_style_context().add_class('discsongsflowbox')
-
-    @GObject.Property(type=int, minimum=1, default=1)
-    def columns(self):
-        """Number of columns for the song list
-
-        :returns: The number of columns
-        :rtype: int
-        """
-        return self._columns
-
-    @columns.setter
-    def columns(self, columns):
-        """Set the number of columns to use
-
-        :param int columns: The number of columns the widget uses
-        """
-        self._columns = columns
-
-        children_n = max(len(self.get_children()), 1)
-
-        if children_n % self._columns == 0:
-            max_per_line = children_n / self._columns
-        else:
-            max_per_line = int(children_n / self._columns) + 1
-
-        self.props.max_children_per_line = max_per_line
-        self.props.min_children_per_line = max_per_line
-
 
 @Gtk.Template(resource_path='/org/gnome/Music/ui/DiscBox.ui')
 class DiscBox(Gtk.Box):
@@ -92,14 +57,15 @@ class DiscBox(Gtk.Box):
     __gtype_name__ = 'DiscBox'
 
     _disc_label = Gtk.Template.Child()
-    _disc_songs_flowbox = Gtk.Template.Child()
+    # _disc_songs_flowbox = Gtk.Template.Child()
+    _list_box = Gtk.Template.Child()
 
     __gsignals__ = {
+        "ready": (GObject.SignalFlags.RUN_FIRST, None, ()),
         'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
         'song-activated': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.Widget,))
     }
 
-    columns = GObject.Property(type=int, minimum=1, default=1)
     selection_mode = GObject.Property(type=bool, default=False)
     selection_mode_allowed = GObject.Property(type=bool, default=True)
     show_disc_label = GObject.Property(type=bool, default=False)
@@ -111,19 +77,15 @@ class DiscBox(Gtk.Box):
         return '<DiscBox>'
 
     @log
-    def __init__(self, model=None):
+    def __init__(self, model):
         """Initialize
 
-        :param model: The TreeStore to use
+        :param model: The Gio.ListStore to use
         """
         super().__init__()
 
         self._model = model
-        self._model.connect('row-changed', self._model_row_changed)
 
-        self.bind_property(
-            'columns', self._disc_songs_flowbox, 'columns',
-            GObject.BindingFlags.SYNC_CREATE)
         self.bind_property(
             'show-disc-label', self._disc_label, 'visible',
             GObject.BindingFlags.SYNC_CREATE)
@@ -132,6 +94,10 @@ class DiscBox(Gtk.Box):
         self._selected_items = []
         self._songs = []
 
+        self._model.connect_after(
+            "items-changed", self._on_model_items_changed)
+        self._list_box.bind_model(self._model, self._create_widget)
+
     @log
     def set_disc_number(self, disc_number):
         """Set the dics number to display
@@ -141,96 +107,46 @@ class DiscBox(Gtk.Box):
         self._disc_label.props.label = _("Disc {}").format(disc_number)
         self._disc_label.props.visible = True
 
-    @log
-    def set_songs(self, songs):
-        """Songs to display
-
-        :param list songs: A list of Grilo media items to
-        add to the widget
-        """
-        for song in songs:
-            song_widget = self._create_song_widget(song)
-            self._disc_songs_flowbox.insert(song_widget, -1)
-            song.song_widget = song_widget
-
-    @log
     def get_selected_items(self):
         """Return all selected items
 
         :returns: The selected items:
         :rtype: A list if Grilo media items
         """
-        self._selected_items = []
-        self._disc_songs_flowbox.foreach(self._get_selected)
-
-        return self._selected_items
-
-    @log
-    def _get_selected(self, child):
-        song_widget = child.get_child()
+        return []
 
-        if song_widget.selected:
-            itr = song_widget.itr
-            self._selected_items.append(self._model[itr][5])
-
-    # FIXME: select all/none slow probably b/c of the row changes
-    # invocations, maybe workaround?
-    @log
     def select_all(self):
         """Select all songs"""
         def child_select_all(child):
             song_widget = child.get_child()
-            self._model[song_widget.itr][6] = True
+            song_widget.props.selected = True
 
-        self._disc_songs_flowbox.foreach(child_select_all)
+        self._list_box.foreach(child_select_all)
 
-    @log
     def select_none(self):
         """Deselect all songs"""
         def child_select_none(child):
             song_widget = child.get_child()
-            self._model[song_widget.itr][6] = False
-
-        self._disc_songs_flowbox.foreach(child_select_none)
+            song_widget.props.selected = False
 
-    @log
-    def _create_song_widget(self, song):
-        """Helper function to create a song widget for a
-        single song
+        self._list_box.foreach(child_select_none)
 
-        :param song: A Grilo media item
-        :returns: A complete song widget
-        :rtype: Gtk.EventBox
-        """
-        song_widget = SongWidget(song)
+    def _create_widget(self, coresong):
+        song_widget = SongWidget(coresong)
         self._songs.append(song_widget)
 
-        title = utils.get_media_title(song)
-
-        itr = self._model.append(None)
-
-        self._model[itr][0, 1, 2, 5, 6] = [title, '', '', song, False]
+        self.bind_property(
+            "selection-mode", song_widget, "selection-mode",
+            GObject.BindingFlags.BIDIRECTIONAL
+            | GObject.BindingFlags.SYNC_CREATE)
 
-        song_widget.itr = itr
-        song_widget.model = self._model
         song_widget.connect('button-release-event', self._song_activated)
-        song_widget.connect('selection-changed', self._on_selection_changed)
-
-        self.bind_property(
-            'selection-mode', song_widget, 'selection-mode',
-            GObject.BindingFlags.SYNC_CREATE)
-        self.bind_property(
-            'show-durations', song_widget, 'show-duration',
-            GObject.BindingFlags.SYNC_CREATE)
-        self.bind_property(
-            'show-favorites', song_widget, 'show-favorite',
-            GObject.BindingFlags.SYNC_CREATE)
-        self.bind_property(
-            'show-song-numbers', song_widget, 'show-song-number',
-            GObject.BindingFlags.SYNC_CREATE)
 
         return song_widget
 
+    def _on_model_items_changed(self, model, position, removed, added):
+        self.emit("ready")
+
     @log
     def _on_selection_changed(self, widget):
         self.emit('selection-changed')
@@ -249,33 +165,21 @@ class DiscBox(Gtk.Box):
                 and not self.props.selection_mode
                 and self.props.selection_mode_allowed):
             self.props.selection_mode = True
+            return
 
         (_, button) = event.get_button()
         if (button == Gdk.BUTTON_PRIMARY
                 and not self.props.selection_mode):
             self.emit('song-activated', widget)
 
-        if self.props.selection_mode:
-            itr = widget.itr
-            self._model[itr][6] = not self._model[itr][6]
-
-        return True
-
-    @log
-    def _model_row_changed(self, model, path, itr):
-        if (not self.props.selection_mode
-                or not model[itr][5]):
-            return
-
-        song_widget = model[itr][5].song_widget
-        selected = model[itr][6]
-        if selected != song_widget.props.selected:
-            song_widget.props.selected = selected
+        # FIXME: Need to ignore the event from the checkbox.
+        # if self.props.selection_mode:
+        #     widget.props.selected = not widget.props.selected
 
         return True
 
 
-class DiscListBox(Gtk.Box):
+class DiscListBox(Gtk.ListBox):
     """A ListBox widget containing all discs of a particular
     album
     """
@@ -293,28 +197,13 @@ class DiscListBox(Gtk.Box):
     @log
     def __init__(self):
         """Initialize"""
-        super().__init__(orientation=Gtk.Orientation.VERTICAL)
+        super().__init__()
 
+        self.props.valign = Gtk.Align.START
         self._selection_mode = False
         self._selected_items = []
 
-    @log
-    def add(self, widget):
-        """Insert a DiscBox widget"""
-        super().add(widget)
-        widget.connect('selection-changed', self._on_selection_changed)
-
-        self.bind_property(
-            'selection-mode', widget, 'selection-mode',
-            GObject.BindingFlags.BIDIRECTIONAL
-            | GObject.BindingFlags.SYNC_CREATE)
-        self.bind_property(
-            'selection-mode-allowed', widget, 'selection-mode-allowed',
-            GObject.BindingFlags.SYNC_CREATE)
-
-    @log
-    def _on_selection_changed(self, widget):
-        self.emit('selection-changed')
+        self.get_style_context().add_class("disc-list-box")
 
     @log
     def get_selected_items(self):
@@ -336,7 +225,7 @@ class DiscListBox(Gtk.Box):
     def select_all(self):
         """Select all songs"""
         def child_select_all(child):
-            child.select_all()
+            child.get_child().select_all()
 
         self.foreach(child_select_all)
 
@@ -344,7 +233,7 @@ class DiscListBox(Gtk.Box):
     def select_none(self):
         """Deselect all songs"""
         def child_select_none(child):
-            child.select_none()
+            child.get_child().select_none()
 
         self.foreach(child_select_none)
 
@@ -367,3 +256,9 @@ class DiscListBox(Gtk.Box):
             return
 
         self._selection_mode = value
+
+        def set_selection_mode(child):
+            print("set selection mode on", child)
+            child.props.selection_mode = value
+
+        self.foreach(set_selection_mode)
diff --git a/gnomemusic/widgets/notificationspopup.py b/gnomemusic/widgets/notificationspopup.py
index 08b2750a..b9befcdd 100644
--- a/gnomemusic/widgets/notificationspopup.py
+++ b/gnomemusic/widgets/notificationspopup.py
@@ -123,14 +123,12 @@ class NotificationsPopup(Gtk.Revealer):
         self.set_reveal_child(True)
 
     @log
-    def remove_notification(self, notification, signal):
-        """Remove notification and emit a signal.
+    def remove_notification(self, notification):
+        """Removes notification.
 
         :param notification: notification to remove
-        :param signal: signal to emit: deletion or undo action
         """
         self._set_visibility(notification, True)
-        notification.emit(signal)
 
     @log
     def terminate_pending(self):
@@ -138,7 +136,7 @@ class NotificationsPopup(Gtk.Revealer):
         children = self._grid.get_children()
         if len(children) > 1:
             for notification in children[:-1]:
-                self.remove_notification(notification, 'finish-deletion')
+                notification._finish_deletion()
 
 
 class LoadingNotification(Gtk.Grid):
@@ -211,41 +209,76 @@ class PlaylistNotification(Gtk.Grid):
         PLAYLIST = 0
         SONG = 1
 
-    __gsignals__ = {
-        'undo-deletion': (GObject.SignalFlags.RUN_FIRST, None, ()),
-        'finish-deletion': (GObject.SignalFlags.RUN_FIRST, None, ())
-    }
-
     def __repr__(self):
         return '<PlaylistNotification>'
 
     @log
-    def __init__(self, notifications_popup, type_, message, data):
+    def __init__(
+            self, notifications_popup, coremodel, type_, playlist,
+            position=None, coresong=None):
+        """Creates a playlist deletion notification popup (song or playlist)
+
+        :param GtkRevealer: notifications_popup: the popup object
+        :param CoreModel: core model
+        :param type_: NotificationType (song or playlist)
+        :param Playlist playlist: playlist
+        :param int position: position of the object to delete
+        :param object coresong: CoreSong for song deletion
+        """
         super().__init__(column_spacing=18)
         self._notifications_popup = notifications_popup
+        self._coremodel = coremodel
         self.type_ = type_
-        self.data = data
+        self._playlist = playlist
+        self._position = position
+        self._coresong = coresong
 
+        message = self._create_notification_message()
         self._label = Gtk.Label(
             label=message, halign=Gtk.Align.START, hexpand=True)
         self.add(self._label)
 
         undo_button = Gtk.Button.new_with_mnemonic(_("_Undo"))
-        undo_button.connect("clicked", self._undo_clicked)
+        undo_button.connect("clicked", self._undo_deletion)
         self.add(undo_button)
         self.show_all()
 
-        self._timeout_id = GLib.timeout_add_seconds(
-            5, self._notifications_popup.remove_notification, self,
-            'finish-deletion')
+        if self.type_ == PlaylistNotification.Type.PLAYLIST:
+            self._coremodel.stage_playlist_deletion(self._playlist)
+        else:
+            playlist.stage_song_deletion(self._coresong, position)
 
+        self._timeout_id = GLib.timeout_add_seconds(5, self._finish_deletion)
         self._notifications_popup.add_notification(self)
 
+    def _create_notification_message(self):
+        if self.type_ == PlaylistNotification.Type.PLAYLIST:
+            msg = _("Playlist {} removed".format(self._playlist.props.title))
+        else:
+            playlist_title = self._playlist.props.title
+            song_title = self._coresong.props.title
+            msg = _("{} removed from {}".format(
+                song_title, playlist_title))
+
+        return msg
+
     @log
-    def _undo_clicked(self, widget_):
+    def _undo_deletion(self, widget_):
         """Undo deletion and remove notification"""
         if self._timeout_id > 0:
             GLib.source_remove(self._timeout_id)
             self._timeout_id = 0
 
-        self._notifications_popup.remove_notification(self, 'undo-deletion')
+        self._notifications_popup.remove_notification(self)
+        if self.type_ == PlaylistNotification.Type.PLAYLIST:
+            self._coremodel.finish_playlist_deletion(self._playlist, False)
+        else:
+            self._playlist.undo_pending_song_deletion(
+                self._coresong, self._position)
+
+    def _finish_deletion(self):
+        self._notifications_popup.remove_notification(self)
+        if self.type_ == PlaylistNotification.Type.PLAYLIST:
+            self._coremodel.finish_playlist_deletion(self._playlist, True)
+        else:
+            self._playlist.finish_song_deletion(self._coresong)
diff --git a/gnomemusic/widgets/playertoolbar.py b/gnomemusic/widgets/playertoolbar.py
index 264758c2..d094597f 100644
--- a/gnomemusic/widgets/playertoolbar.py
+++ b/gnomemusic/widgets/playertoolbar.py
@@ -168,16 +168,16 @@ class PlayerToolbar(Gtk.ActionBar):
 
         :param Player player: The main player object
         """
-        current_song = player.props.current_song
-        self._duration_label.set_label(
-            utils.seconds_to_string(current_song.get_duration()))
+        coresong = player.props.current_song
+        self._duration_label.props.label = utils.seconds_to_string(
+            coresong.props.duration)
         self._progress_time_label.props.label = "0:00"
 
         self._play_button.set_sensitive(True)
         self._sync_prev_next()
 
-        artist = utils.get_artist_name(current_song)
-        title = utils.get_media_title(current_song)
+        artist = coresong.props.artist
+        title = coresong.props.title
 
         self._title_label.props.label = title
         self._artist_label.props.label = artist
@@ -185,7 +185,7 @@ class PlayerToolbar(Gtk.ActionBar):
         self._tooltip.props.title = title
         self._tooltip.props.subtitle = artist
 
-        self._cover_stack.update(current_song)
+        self._cover_stack.update(coresong)
 
     @Gtk.Template.Callback()
     @log
diff --git a/gnomemusic/widgets/playlistcontrols.py b/gnomemusic/widgets/playlistcontrols.py
index 014b2943..9a08a397 100644
--- a/gnomemusic/widgets/playlistcontrols.py
+++ b/gnomemusic/widgets/playlistcontrols.py
@@ -27,16 +27,13 @@ import gettext
 from gi.repository import Gdk, GObject, Gtk
 
 from gnomemusic import log
+from gnomemusic.grilowrappers.grltrackerplaylists import Playlist
 
 
 @Gtk.Template(resource_path='/org/gnome/Music/ui/PlaylistControls.ui')
 class PlaylistControls(Gtk.Grid):
     """Widget holding the playlist controls"""
 
-    __gsignals__ = {
-        'playlist-renamed': (GObject.SignalFlags.RUN_FIRST, None, (str,))
-    }
-
     __gtype_name__ = "PlaylistControls"
 
     _name_stack = Gtk.Template.Child()
@@ -46,19 +43,15 @@ class PlaylistControls(Gtk.Grid):
     _songs_count_label = Gtk.Template.Child()
     _menubutton = Gtk.Template.Child()
 
-    songs_count = GObject.Property(
-        type=int, default=0, minimum=0, flags=GObject.ParamFlags.READWRITE)
-    playlist_name = GObject.Property(
-        type=str, default="", flags=GObject.ParamFlags.READWRITE)
-
     def __repr__(self):
         return '<PlaylistControls>'
 
     def __init__(self):
         super().__init__()
-        self.bind_property("playlist-name", self._name_label, "label")
 
-        self.connect("notify::songs-count", self._on_songs_count_changed)
+        self._playlist = None
+        self._count_id = 0
+        self._binding_count = None
 
     @Gtk.Template.Callback()
     @log
@@ -81,15 +74,14 @@ class PlaylistControls(Gtk.Grid):
         if not new_name:
             return
 
-        self.props.playlist_name = new_name
+        self.props.playlist.props.title = new_name
         self.disable_rename_playlist()
-        self.emit('playlist-renamed', new_name)
 
     @log
     def _on_songs_count_changed(self, klass, data=None):
         self._songs_count_label.props.label = gettext.ngettext(
-            "{} Song", "{} Songs", self.props.songs_count).format(
-                self.props.songs_count)
+            "{} Song", "{} Songs", self.props.playlist.count).format(
+                self.props.playlist.count)
 
     @log
     def enable_rename_playlist(self, pl_torename):
@@ -128,3 +120,32 @@ class PlaylistControls(Gtk.Grid):
     def _set_rename_entry_text_and_focus(self, text):
         self._rename_entry.props.text = text
         self._rename_entry.grab_focus()
+
+    @GObject.Property(
+        type=Playlist, default=None, flags=GObject.ParamFlags.READWRITE)
+    def playlist(self):
+        """Playlist property getter.
+
+        :returns: current playlist
+        :rtype: Playlist
+        """
+        return self._playlist
+
+    @playlist.setter
+    def playlist(self, new_playlist):
+        """Playlist property setter.
+
+        :param Playlistnew_playlist: new playlist
+        """
+        if self._count_id > 0:
+            self._playlist.disconnect(self._count_id)
+            self._count_id = 0
+            self._binding_count.unbind()
+
+        self._playlist = new_playlist
+        self._binding_count = self._playlist.bind_property(
+            "title", self._name_label, "label",
+            GObject.BindingFlags.SYNC_CREATE)
+        self._count_id = self._playlist.connect(
+            "notify::count", self._on_songs_count_changed)
+        self._on_songs_count_changed(None)
diff --git a/gnomemusic/widgets/playlistdialog.py b/gnomemusic/widgets/playlistdialog.py
index e328f422..bd042f41 100644
--- a/gnomemusic/widgets/playlistdialog.py
+++ b/gnomemusic/widgets/playlistdialog.py
@@ -22,10 +22,10 @@
 # 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 gi.repository import Gtk
+from gi.repository import GObject, Gtk
 
 from gnomemusic import log
-from gnomemusic.playlists import Playlists
+from gnomemusic.grilowrappers.grltrackerplaylists import Playlist
 from gnomemusic.widgets.playlistdialogrow import PlaylistDialogRow
 
 
@@ -35,6 +35,8 @@ class PlaylistDialog(Gtk.Dialog):
 
     __gtype_name__ = 'PlaylistDialog'
 
+    selected_playlist = GObject.Property(type=Playlist, default=None)
+
     _add_playlist_stack = Gtk.Template.Child()
     _normal_box = Gtk.Template.Child()
     _empty_box = Gtk.Template.Child()
@@ -60,22 +62,13 @@ class PlaylistDialog(Gtk.Dialog):
         self.props.transient_for = parent
         self.set_titlebar(self._title_bar)
 
+        # FIXME: should we use a special model without the smart playlists?
         self._user_playlists_available = False
-        self._playlists = Playlists.get_default()
-        playlists_model = self._playlists.get_user_playlists()
+        self._coremodel = parent._app.props.coremodel
         self._listbox.bind_model(
-            playlists_model, self._create_playlist_row)
-        self._set_view()
-
-    @log
-    def get_selected(self):
-        """Get the selected playlist"""
-        selected_row = self._listbox.get_selected_row()
+            self._coremodel.props.playlists_sort, self._create_playlist_row)
 
-        if not selected_row:
-            return None
-
-        return selected_row.props.playlist
+        self._set_view()
 
     @log
     def _set_view(self):
@@ -93,6 +86,9 @@ class PlaylistDialog(Gtk.Dialog):
     @log
     def _create_playlist_row(self, playlist):
         """Adds (non-smart only) playlists to the model"""
+        if playlist.props.is_smart:
+            return None
+
         self._user_playlists_available = True
         self._set_view()
 
@@ -116,6 +112,8 @@ class PlaylistDialog(Gtk.Dialog):
         self._add_playlist_entry.props.text = ""
         self._add_playlist_button.props.sensitive = False
         selected_row = self._listbox.get_selected_row()
+        if selected_row is not None:
+            self.props.selected_playlist = selected_row.props.playlist
         self._select_button.props.sensitive = selected_row is not None
 
         for row in self._listbox:
@@ -133,7 +131,7 @@ class PlaylistDialog(Gtk.Dialog):
 
         text = self._add_playlist_entry.props.text
         if text:
-            self._playlists.create_playlist(text, select_and_close_dialog)
+            self._coremodel.create_playlist(text, select_and_close_dialog)
 
     @Gtk.Template.Callback()
     @log
diff --git a/gnomemusic/widgets/playlistdialogrow.py b/gnomemusic/widgets/playlistdialogrow.py
index 1e27b2bc..030c17ff 100644
--- a/gnomemusic/widgets/playlistdialogrow.py
+++ b/gnomemusic/widgets/playlistdialogrow.py
@@ -24,7 +24,7 @@
 
 from gi.repository import GObject, Gtk
 
-from gnomemusic.playlists import Playlist
+from gnomemusic.grilowrappers.grltrackerplaylists import Playlist
 
 
 @Gtk.Template(resource_path="/org/gnome/Music/ui/PlaylistDialogRow.ui")
diff --git a/gnomemusic/widgets/playlisttile.py b/gnomemusic/widgets/playlisttile.py
new file mode 100644
index 00000000..7667bacd
--- /dev/null
+++ b/gnomemusic/widgets/playlisttile.py
@@ -0,0 +1,58 @@
+# Copyright 2019 The GNOME Music developers
+#
+# GNOME Music is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GNOME Music is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The GNOME Music authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and GNOME Music.  This permission is above and beyond the permissions
+# granted by the GPL license by which GNOME Music is covered.  If you
+# modify this code, you may extend this exception to your version of the
+# 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 gi.repository import GObject, Gtk
+
+from gnomemusic import log
+from gnomemusic.grilowrappers.grltrackerplaylists import Playlist
+
+
+@Gtk.Template(resource_path="/org/gnome/Music/ui/PlaylistTile.ui")
+class PlaylistTile(Gtk.ListBoxRow):
+    """Row for sidebars
+
+    Contains a label and an optional checkbox.
+    """
+
+    __gtype_name__ = "PlaylistTile"
+
+    _label = Gtk.Template.Child()
+
+    playlist = GObject.Property(type=Playlist, default=None)
+    text = GObject.Property(type=str, default='')
+
+    def __repr__(self):
+        return "<PlaylistTile>"
+
+    @log
+    def __init__(self, playlist):
+        super().__init__()
+
+        self.props.playlist = playlist
+
+        self.props.playlist.bind_property(
+            "title", self._label, "label", GObject.BindingFlags.SYNC_CREATE)
+        self.props.playlist.bind_property(
+            "title", self._label, "tooltip-text",
+            GObject.BindingFlags.SYNC_CREATE)
diff --git a/gnomemusic/widgets/searchbar.py b/gnomemusic/widgets/searchbar.py
index 4d5c07a7..f391d64a 100644
--- a/gnomemusic/widgets/searchbar.py
+++ b/gnomemusic/widgets/searchbar.py
@@ -32,7 +32,6 @@ from gi.repository import Gd, GLib, GObject, Gtk, Pango
 from gi.repository.Gd import TaggedEntry  # noqa: F401
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
 from gnomemusic.search import Search
 
 
@@ -118,7 +117,7 @@ class SourceManager(BaseManager):
         self.values.append(['grl-tracker-source', _("Local"), ''])
         self.props.default_value = 2
 
-        grilo.connect('new-source-added', self._add_new_source)
+        # grilo.connect('new-source-added', self._add_new_source)
 
     @log
     def fill_in_values(self, model):
@@ -144,8 +143,8 @@ class SourceManager(BaseManager):
 
         Adds available Grilo sources to the internal model.
         """
-        for id_ in grilo.props.sources:
-            self._add_new_source(None, grilo.props.sources[id_])
+        # for id_ in grilo.props.sources:
+        #     self._add_new_source(None, grilo.props.sources[id_])
 
     @GObject.Property
     def active(self):
@@ -159,8 +158,8 @@ class SourceManager(BaseManager):
         # https://gitlab.gnome.org/GNOME/gnome-music/snippets/31
         super(SourceManager, self.__class__).active.fset(self, selected_id)
 
-        src = grilo.sources[selected_id] if selected_id != 'all' else None
-        grilo.search_source = src
+        # src = grilo.sources[selected_id] if selected_id != 'all' else None
+        # grilo.search_source = src
 
 
 @Gtk.Template(resource_path="/org/gnome/Music/ui/FilterView.ui")
@@ -317,7 +316,8 @@ class DropDown(Gtk.Revealer):
 
     @log
     def _is_tracker(self, grilo_id):
-        return grilo_id == "grl-tracker-source"
+        return True
+        # return grilo_id == "grl-tracker-source"
 
 
 @Gtk.Template(resource_path="/org/gnome/Music/ui/SearchBar.ui")
@@ -337,10 +337,11 @@ class SearchBar(Gtk.SearchBar):
         return '<SearchBar>'
 
     @log
-    def __init__(self):
+    def __init__(self, application):
         """Initialize the SearchBar"""
         super().__init__()
 
+        self._application = application
         self._timeout = None
 
         self._dropdown = DropDown()
@@ -375,15 +376,17 @@ class SearchBar(Gtk.SearchBar):
         self._timeout = None
 
         search_term = self._search_entry.get_text()
-        if grilo.search_source:
-            fields_filter = self._dropdown.search_manager.active
-        else:
-            fields_filter = 'search_all'
+        # if grilo.search_source:
+        #     fields_filter = self._dropdown.search_manager.active
+        # else:
+        #     fields_filter = 'search_all'
 
         if search_term != "":
             self.props.stack.set_visible_child_name('search')
-            view = self.props.stack.get_visible_child()
-            view.set_search_text(search_term, fields_filter)
+
+            self._application._coremodel.search(search_term)
+            # view = self.props.stack.get_visible_child()
+            # view.set_search_text(search_term, fields_filter)
         else:
             self._set_error_style(False)
 
diff --git a/gnomemusic/widgets/selectiontoolbar.py b/gnomemusic/widgets/selectiontoolbar.py
index 1d2b775a..16c9472b 100644
--- a/gnomemusic/widgets/selectiontoolbar.py
+++ b/gnomemusic/widgets/selectiontoolbar.py
@@ -50,6 +50,8 @@ class SelectionToolbar(Gtk.ActionBar):
         self.connect(
             'notify::selected-items-count', self._on_item_selection_changed)
 
+        self.notify("selected-items-count")
+
     @Gtk.Template.Callback()
     @log
     def _on_add_to_playlist_button_clicked(self, widget):
diff --git a/gnomemusic/widgets/songwidget.py b/gnomemusic/widgets/songwidget.py
index f3310d94..365ccfd9 100644
--- a/gnomemusic/widgets/songwidget.py
+++ b/gnomemusic/widgets/songwidget.py
@@ -31,8 +31,7 @@ from gi.repository.Dazzle import BoldingLabel  # noqa: F401
 
 from gnomemusic import log
 from gnomemusic import utils
-from gnomemusic.grilo import grilo
-from gnomemusic.playlists import Playlists
+from gnomemusic.coresong import CoreSong
 from gnomemusic.widgets.starimage import StarImage  # noqa: F401
 
 
@@ -53,15 +52,20 @@ class SongWidget(Gtk.EventBox):
 
     __gsignals__ = {
         'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
+        "widget-moved": (GObject.SignalFlags.RUN_FIRST, None, (int,))
     }
 
+    coresong = GObject.Property(type=CoreSong, default=None)
     selected = GObject.Property(type=bool, default=False)
     show_duration = GObject.Property(type=bool, default=True)
     show_favorite = GObject.Property(type=bool, default=True)
     show_song_number = GObject.Property(type=bool, default=True)
 
-    _playlists = Playlists.get_default()
-
+    _album_label = Gtk.Template.Child()
+    _album_duration_box = Gtk.Template.Child()
+    _artist_box = Gtk.Template.Child()
+    _artist_label = Gtk.Template.Child()
+    _dnd_eventbox = Gtk.Template.Child()
     _select_button = Gtk.Template.Child()
     _number_label = Gtk.Template.Child()
     _title_label = Gtk.Template.Child()
@@ -69,6 +73,7 @@ class SongWidget(Gtk.EventBox):
     _star_eventbox = Gtk.Template.Child()
     _star_image = Gtk.Template.Child()
     _play_icon = Gtk.Template.Child()
+    _size_group = Gtk.Template.Child()
 
     class State(IntEnum):
         """The state of the SongWidget
@@ -81,27 +86,42 @@ class SongWidget(Gtk.EventBox):
         return '<SongWidget>'
 
     @log
-    def __init__(self, media):
+    def __init__(self, coresong, can_dnd=False, show_artist_and_album=False):
+        """Instanciates a SongWidget
+
+        :param Corsong coresong: song associated with the widget
+        :param bool can_dnd: allow drag and drop operations
+        :param bool show_artist_and_album: display artist and album
+        """
         super().__init__()
 
-        self._media = media
+        self.props.coresong = coresong
+        self._media = self.props.coresong.props.media
         self._selection_mode = False
         self._state = SongWidget.State.UNPLAYED
 
-        song_number = media.get_track_number()
+        song_number = self.props.coresong.props.track_number
         if song_number == 0:
             song_number = ""
         self._number_label.set_text(str(song_number))
 
-        title = utils.get_media_title(media)
+        title = self.props.coresong.props.title
         self._title_label.set_max_width_chars(50)
         self._title_label.set_text(title)
         self._title_label.props.tooltip_text = title
 
-        time = utils.seconds_to_string(media.get_duration())
-        self._duration_label.set_text(time)
+        time = utils.seconds_to_string(self.props.coresong.props.duration)
+        self._duration_label.props.label = time
 
-        self._star_image.props.favorite = media.get_favourite()
+        if show_artist_and_album is True:
+            album = self.props.coresong.props.album
+            self._album_label.props.label = album
+            self._album_label.props.visible = True
+            artist = self.props.coresong.props.artist
+            self._artist_label.props.label = artist
+            self._artist_box.props.visible = True
+        else:
+            self._size_group.remove_widget(self._album_duration_box)
 
         self._select_button.set_visible(False)
 
@@ -109,7 +129,7 @@ class SongWidget(Gtk.EventBox):
             'media-playback-start-symbolic', Gtk.IconSize.SMALL_TOOLBAR)
         self._play_icon.set_no_show_all(True)
 
-        self.bind_property(
+        self.props.coresong.bind_property(
             'selected', self._select_button, 'active',
             GObject.BindingFlags.BIDIRECTIONAL
             | GObject.BindingFlags.SYNC_CREATE)
@@ -124,13 +144,72 @@ class SongWidget(Gtk.EventBox):
         self.bind_property(
             'show-song-number', self._number_label, 'visible',
             GObject.BindingFlags.SYNC_CREATE)
-        self._number_label.set_no_show_all(True)
+        self.props.coresong.bind_property(
+            "favorite", self._star_image, "favorite",
+            GObject.BindingFlags.BIDIRECTIONAL
+            | GObject.BindingFlags.SYNC_CREATE)
+        self.props.coresong.bind_property(
+            "state", self, "state",
+            GObject.BindingFlags.SYNC_CREATE)
+        self.props.coresong.connect(
+            "notify::validation", self._on_validation_changed)
+
+        self._number_label.props.no_show_all = True
+
+        if can_dnd is True:
+            self._dnd_eventbox.props.visible = True
+            self._drag_widget = None
+            entries = [
+                Gtk.TargetEntry.new(
+                    "GTK_EVENT_BOX", Gtk.TargetFlags.SAME_APP, 0)
+            ]
+            self._dnd_eventbox.drag_source_set(
+                Gdk.ModifierType.BUTTON1_MASK, entries,
+                Gdk.DragAction.MOVE)
+            self.drag_dest_set(
+                Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
 
     @Gtk.Template.Callback()
     @log
     def _on_selection_changed(self, klass, value):
         self.emit('selection-changed')
 
+    @Gtk.Template.Callback()
+    def _on_drag_begin(self, klass, context):
+        gdk_window = self.get_window()
+        _, x, y, _ = gdk_window.get_device_position(context.get_device())
+        allocation = self.get_allocation()
+
+        self._drag_widget = Gtk.ListBox()
+        self._drag_widget.set_size_request(allocation.width, allocation.height)
+
+        drag_row = SongWidget(self.props.coresong)
+        self._drag_widget.add(drag_row)
+        self._drag_widget.drag_highlight_row(drag_row.get_parent())
+        self._drag_widget.show_all()
+        Gtk.drag_set_icon_widget(context, self._drag_widget, x, y)
+
+    @Gtk.Template.Callback()
+    def _on_drag_end(self, klass, context):
+        self._drag_widget = None
+
+    @Gtk.Template.Callback()
+    def _on_drag_data_get(self, klass, context, selection_data, info, time_):
+        row_position = self.get_parent().get_index()
+        selection_data.set(
+            Gdk.Atom.intern("row_position", False), 0,
+            bytes(str(row_position), encoding="UTF8"))
+
+    @Gtk.Template.Callback()
+    def _on_drag_data_received(
+            self, klass, context, x, y, selection_data, info, time_):
+        source_position = int(str(selection_data.get_data(), "UTF-8"))
+        target_position = self.get_parent().get_index()
+        if source_position == target_position:
+            return
+
+        self.emit("widget-moved", source_position)
+
     @Gtk.Template.Callback()
     @log
     def _on_star_toggle(self, widget, event):
@@ -141,11 +220,6 @@ class SongWidget(Gtk.EventBox):
         favorite = not self._star_image.favorite
         self._star_image.props.favorite = favorite
 
-        # TODO: Rework and stop updating widgets from here directly.
-        grilo.set_favorite(self._media, favorite)
-        favorite_playlist = self._playlists.get_smart_playlist("Favorites")
-        self._playlists.update_smart_playlist(favorite_playlist)
-
         return True
 
     @Gtk.Template.Callback()
@@ -205,8 +279,22 @@ class SongWidget(Gtk.EventBox):
         style_ctx.remove_class('playing-song-label')
         self._play_icon.set_visible(False)
 
+        coresong = self.props.coresong
+        if coresong.props.validation == CoreSong.Validation.FAILED:
+            self._play_icon.set_visible(True)
+            style_ctx.add_class("dim-label")
+            return
+
         if value == SongWidget.State.PLAYED:
             style_ctx.add_class('dim-label')
         elif value == SongWidget.State.PLAYING:
             self._play_icon.set_visible(True)
             style_ctx.add_class('playing-song-label')
+
+    def _on_validation_changed(self, coresong, sate):
+        validation_status = coresong.props.validation
+        if validation_status == CoreSong.Validation.FAILED:
+            self._play_icon.props.icon_name = "dialog-error-symbolic"
+            self._play_icon.set_visible(True)
+        else:
+            self._play_icon.props.icon_name = "media-playback-start-symbolic"
diff --git a/gnomemusic/widgets/starhandlerwidget.py b/gnomemusic/widgets/starhandlerwidget.py
index e1745602..5e276de4 100644
--- a/gnomemusic/widgets/starhandlerwidget.py
+++ b/gnomemusic/widgets/starhandlerwidget.py
@@ -25,10 +25,6 @@
 from gi.repository import GObject, Gtk
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
-from gnomemusic.playlists import Playlists
-
-playlists = Playlists.get_default()
 
 
 class CellRendererStar(Gtk.CellRendererPixbuf):
@@ -137,23 +133,22 @@ class StarHandlerWidget(object):
     @log
     def _on_star_toggled(self, widget, path):
         """Called if a star is clicked"""
+        model = self._parent._view.props.model
         try:
-            _iter = self._parent.model.get_iter(path)
+            _iter = model.get_iter(path)
         except ValueError:
             return
 
         try:
-            if self._parent.model[_iter][self._star_index] == 2:
+            if model[_iter][self._star_index] == 2:
                 return
         except AttributeError:
             return
 
-        new_value = not self._parent.model[_iter][self._star_index]
-        self._parent.model[_iter][self._star_index] = new_value
-        song_item = self._parent.model[_iter][5]
-        grilo.toggle_favorite(song_item)
-        favorite_playlist = playlists.get_smart_playlist("Favorites")
-        playlists.update_smart_playlist(favorite_playlist)
+        new_value = not model[_iter][self._star_index]
+        model[_iter][self._star_index] = new_value
+        coresong = model[_iter][7]
+        coresong.props.favorite = new_value
 
         # Use this flag to ignore the upcoming _on_item_activated call
         self.star_renderer_click = True
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index 6e747cbd..81eb97d4 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -26,12 +26,9 @@ from gi.repository import Gtk, Gdk, Gio, GLib, GObject
 from gettext import gettext as _
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
 from gnomemusic.gstplayer import Playback
 from gnomemusic.mediakeys import MediaKeys
 from gnomemusic.player import RepeatMode
-from gnomemusic.playlists import Playlists
-from gnomemusic.query import Query
 from gnomemusic.search import Search
 from gnomemusic.utils import View
 from gnomemusic.views.albumsview import AlbumsView
@@ -51,8 +48,6 @@ from gnomemusic.windowplacement import WindowPlacement
 import logging
 logger = logging.getLogger(__name__)
 
-playlists = Playlists.get_default()
-
 
 @Gtk.Template(resource_path="/org/gnome/Music/ui/Window.ui")
 class Window(Gtk.ApplicationWindow):
@@ -79,6 +74,12 @@ class Window(Gtk.ApplicationWindow):
         """
         super().__init__(application=app, title=_("Music"))
 
+        # Hack
+        self._app = app
+
+        self._app._coreselection.bind_property(
+            "selected-items-count", self, "selected-items-count")
+
         self._settings = app.props.settings
         self.add_action(self._settings.create_action('repeat'))
         select_all = Gio.SimpleAction.new('select_all', None)
@@ -100,34 +101,10 @@ class Window(Gtk.ApplicationWindow):
 
         MediaKeys(self._player, self)
 
-        grilo.connect('changes-pending', self._on_changes_pending)
-
-    @log
-    def _on_changes_pending(self, data=None):
-        # FIXME: This is not working right.
-        def songs_available_cb(available):
-            view_count = len(self.views)
-            if (available
-                    and view_count == 1):
-                self._switch_to_player_view()
-            elif (not available
-                    and not self.props.selection_mode
-                    and view_count != 1):
-                self._stack.disconnect(self._on_notify_model_id)
-                self.disconnect(self._key_press_event_id)
-
-                for i in range(View.ALBUM, view_count):
-                    view = self.views.pop()
-                    view.destroy()
-
-                self._switch_to_empty_view()
-
-        grilo.songs_available(songs_available_cb)
-
     @log
     def _setup_view(self):
         self._search = Search()
-        self._searchbar = SearchBar()
+        self._searchbar = SearchBar(self._app)
         self._searchbar.props.stack = self._stack
         self._headerbar = HeaderBar()
 
@@ -192,14 +169,11 @@ class Window(Gtk.ApplicationWindow):
         self._headerbar.props.state = HeaderBar.State.MAIN
         self._headerbar.show()
 
-        def songs_available_cb(available):
-            if available:
-                self._switch_to_player_view()
-            else:
-                self._switch_to_empty_view()
+        self._app.props.coremodel.connect(
+            "notify::songs-available", self._on_songs_available)
 
-        if Query().music_folder:
-            grilo.songs_available(songs_available_cb)
+        if self._app.props.coremodel.props.songs_available:
+            self._switch_to_player_view()
         else:
             self._switch_to_empty_view()
 
@@ -207,15 +181,25 @@ class Window(Gtk.ApplicationWindow):
     def _switch_to_empty_view(self):
         did_initial_state = self._settings.get_boolean('did-initial-state')
 
-        if not grilo.props.tracker_available:
+        # FIXME: Tracker just checks for TrackerWrapper right now.
+        # It should also check for the viability of certain queries to
+        # make sure we have a recent version available.
+        if not self._app.props.coremodel._grilo.props.tracker_available:
             self.views[View.EMPTY].props.state = EmptyView.State.NO_TRACKER
         elif did_initial_state:
             self.views[View.EMPTY].props.state = EmptyView.State.EMPTY
         else:
+            # FIXME: On switch back this view does not show properly.
             self.views[View.EMPTY].props.state = EmptyView.State.INITIAL
 
         self._headerbar.props.state = HeaderBar.State.EMPTY
 
+    def _on_songs_available(self, klass, value):
+        if self._app.props.coremodel.props.songs_available:
+            self._switch_to_player_view()
+        else:
+            self._switch_to_empty_view()
+
     @log
     def _switch_to_player_view(self):
         self._settings.set_boolean('did-initial-state', True)
@@ -478,26 +462,24 @@ class Window(Gtk.ApplicationWindow):
         if (not self.props.selection_mode
                 and self._player.state == Playback.STOPPED):
             self._player_toolbar.hide()
-        if not self.props.selection_mode:
-            self._on_changes_pending()
 
     @log
     def _on_add_to_playlist(self, widget):
         if self._stack.get_visible_child() == self.views[View.PLAYLIST]:
             return
 
-        def callback(selected_songs):
-            if len(selected_songs) < 1:
-                return
+        selected_songs = self._app._coreselection.props.selected_items
+
+        if len(selected_songs) < 1:
+            return
 
-            playlist_dialog = PlaylistDialog(self)
-            if playlist_dialog.run() == Gtk.ResponseType.ACCEPT:
-                playlists.add_to_playlist(
-                    playlist_dialog.get_selected(), selected_songs)
-            self.props.selection_mode = False
-            playlist_dialog.destroy()
+        playlist_dialog = PlaylistDialog(self)
+        if playlist_dialog.run() == Gtk.ResponseType.ACCEPT:
+            playlist = playlist_dialog.props.selected_playlist
+            playlist.add_songs(selected_songs)
 
-        self._stack.get_visible_child().get_selected_songs(callback)
+        self.props.selection_mode = False
+        playlist_dialog.destroy()
 
     @log
     def set_player_visible(self, visible):
diff --git a/meson.build b/meson.build
index 83b1be5c..014f39ff 100644
--- a/meson.build
+++ b/meson.build
@@ -58,6 +58,12 @@ subproject('libgd',
         'pkglibdir=' + PKGLIB_DIR
     ])
 
+subproject('gfm',
+    default_options: [
+        'pkgdatadir=' + PKGDATA_DIR,
+        'pkglibdir=' + PKGLIB_DIR
+    ])
+
 subdir('data/ui')
 subdir('data')
 subdir('help')
@@ -76,9 +82,10 @@ bin_config.set('pkgdatadir', PKGDATA_DIR)
 bin_config.set('localedir', join_paths(get_option('prefix'), get_option('datadir'), 'locale'))
 bin_config.set('pythondir', PYTHON_DIR)
 bin_config.set('pyexecdir', py_installation.get_path('stdlib'))
-bin_config.set('schemasdir', '')
-# Used for libgd
+bin_config.set('schemasdir', PKGDATA_DIR)
+# Used for libgd/gfm
 bin_config.set('pkglibdir', PKGLIB_DIR)
+bin_config.set('gfmlibdir', '')
 
 bin_config.set('local_build', 'False')
 
@@ -98,8 +105,9 @@ local_config.set('localedir', join_paths(get_option('prefix'), get_option('datad
 local_config.set('pythondir', meson.source_root())
 local_config.set('pyexecdir', meson.source_root())
 local_config.set('schemasdir', join_paths(meson.build_root(), 'data'))
-# Used for libgd
+# Used for libgd/gfm
 local_config.set('pkglibdir', join_paths(meson.build_root(), 'subprojects', 'libgd', 'libgd'))
+local_config.set('gfmlibdir', join_paths(meson.build_root(), 'subprojects', 'gfm'))
 
 local_config.set('local_build', 'True')
 
diff --git a/org.gnome.Music.json b/org.gnome.Music.json
index 96e90aeb..eb78df51 100644
--- a/org.gnome.Music.json
+++ b/org.gnome.Music.json
@@ -99,11 +99,12 @@
         {
             "name": "grilo",
             "buildsystem": "meson",
-           "config-opts": [ "-Denable-gtk-doc=false" ],
+            "config-opts": [ "-Denable-gtk-doc=false" ],
             "sources": [
                 {
                     "type": "git",
-                    "url": "https://gitlab.gnome.org/GNOME/grilo.git";
+                    "url": "https://gitlab.gnome.org/GNOME/grilo.git";,
+                    "branch": "wip/mschraal/fix-grilo-0.3.8"
                 }
             ],
             "cleanup": [ "/include", "/bin" ]
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0fea8e4b..9dfde76f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -17,12 +17,10 @@ data/ui/SelectionToolbar.ui
 gnomemusic/__init__.py
 gnomemusic/albumartcache.py
 gnomemusic/application.py
-gnomemusic/grilo.py
+gnomemusic/grilowrappers/grltrackerplaylist.py
 gnomemusic/gstplayer.py
 gnomemusic/inhibitsuspend.py
 gnomemusic/mpris.py
-gnomemusic/playlists.py
-gnomemusic/query.py
 gnomemusic/utils.py
 gnomemusic/views/albumsview.py
 gnomemusic/views/artistsview.py
diff --git a/subprojects/gfm b/subprojects/gfm
new file mode 160000
index 00000000..956deaca
--- /dev/null
+++ b/subprojects/gfm
@@ -0,0 +1 @@
+Subproject commit 956deaca5ac104f5329bfb542c05d4a63578ee08



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