[gnome-music/wip/mschraal/core: 1/3] playlistview: Very basic functional



commit 1c210aa4f9387017f811e6870d0985d2c9972929
Author: Marinus Schraal <mschraal gnome org>
Date:   Sat Jul 6 17:04:44 2019 +0200

    playlistview: Very basic functional

 gnomemusic/coremodel.py                         |  64 +++
 gnomemusic/grilowrappers/grltrackerplaylists.py | 449 +++++++++++++++++
 gnomemusic/grilowrappers/grltrackersource.py    |   3 +
 gnomemusic/views/playlistsview.py               | 635 +++---------------------
 4 files changed, 575 insertions(+), 576 deletions(-)
---
diff --git a/gnomemusic/coremodel.py b/gnomemusic/coremodel.py
index a895e689..8d7c4d78 100644
--- a/gnomemusic/coremodel.py
+++ b/gnomemusic/coremodel.py
@@ -1,3 +1,5 @@
+import math
+
 import gi
 gi.require_versions({'Dazzle': '1.0', 'Gfm': '0.1'})
 from gi.repository import Dazzle, GObject, Gio, Gfm, Gtk
@@ -7,6 +9,7 @@ from gnomemusic import log
 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
@@ -68,6 +71,14 @@ class CoreModel(GObject.GObject):
             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)
 
     def _filter_selected(self, coresong):
@@ -81,6 +92,24 @@ class CoreModel(GObject.GObject):
         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):
@@ -232,6 +261,30 @@ class CoreModel(GObject.GObject):
                     "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)
+
+                    if model_song is coresong:
+                        song.props.state = SongWidget.State.PLAYING
+
+                    song.bind_property(
+                        "state", model_song, "state",
+                        GObject.BindingFlags.SYNC_CREATE)
+
+                # self._search_signal_id = self._song_search_model.connect(
+                #     "items-changed", _on_items_changed)
+
+                self.emit("playlist-loaded")
 
     def search(self, text):
         self._grilo.search(text)
@@ -296,3 +349,14 @@ class CoreModel(GObject.GObject):
         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
diff --git a/gnomemusic/grilowrappers/grltrackerplaylists.py b/gnomemusic/grilowrappers/grltrackerplaylists.py
new file mode 100644
index 00000000..5fff7493
--- /dev/null
+++ b/gnomemusic/grilowrappers/grltrackerplaylists.py
@@ -0,0 +1,449 @@
+import time
+
+from gettext import gettext as _
+
+import gi
+gi.require_versions({"Grl": "0.3", 'Tracker': "2.0"})
+from gi.repository import Gio, Grl, GLib, GObject
+
+from gnomemusic.coresong import CoreSong
+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_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._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):
+        smart_playlists = {
+            "MostPlayed": MostPlayed(),
+            "NeverPlayed": NeverPlayed(),
+            "RecentlyPlayed": RecentlyPlayed(),
+            "RecentlyAdded": RecentlyAdded(),
+            "Favorites": Favorites()
+        }
+
+        for playlist in smart_playlists.values():
+
+            def _add_to_model(
+                    source, op_id, media, remaining, user_data, error):
+                if error:
+                    print("ERROR", error)
+                    return
+
+                if not media:
+                    user_data.props.count = user_data.props.model.get_n_items()
+                    return
+
+                coresong = CoreSong(media, self._coreselection, self._grilo)
+                user_data.props.model.append(coresong)
+
+            options = self._fast_options.copy()
+
+            self._source.query(
+                playlist.props.query, self.METADATA_KEYS, options,
+                _add_to_model, playlist)
+
+            self._model.append(playlist)
+
+        self._all_user_playlists()
+
+    def _all_user_playlists(self):
+        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, param, item, data, error):
+        if not item:
+            return
+
+        playlist = Playlist(
+            pl_id=item.get_id(), title=utils.get_media_title(item),
+            creation_date=item.get_creation_date(), source=self._source,
+            coremodel=self._coremodel, coreselection=self._coreselection,
+            grilo=self._grilo)
+
+        self._model.append(playlist)
+
+
+class Playlist(GObject.GObject):
+    """ Base class of all playlists """
+
+    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, pl_id=None, query=None, tag_text=None, title=None,
+            creation_date=None, source=None, coremodel=None,
+            coreselection=None, grilo=None):
+        super().__init__()
+
+        self.props.pl_id = pl_id
+        self.props.query = query
+        self.props.tag_text = tag_text
+        self.props.title = title
+        self.props.creation_date = creation_date
+        self._model = None
+        self._source = source
+        self._coremodel = coremodel
+        self._coreselection = coreselection
+        self._grilo = grilo
+
+    @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()
+                return
+
+            coresong = CoreSong(media, self._coreselection, self._grilo)
+            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)
+
+
+class SmartPlaylist(Playlist):
+    """Base class for smart playlists"""
+
+    def __repr__(self):
+        return "<SmartPlaylist>"
+
+    def __init__(self):
+        super().__init__()
+
+        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)
+
+        return self._model
+
+
+class MostPlayed(SmartPlaylist):
+    """Most Played smart playlist"""
+
+    def __init__(self):
+        super().__init__()
+
+        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):
+        super().__init__()
+
+        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):
+        super().__init__()
+
+        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):
+        super().__init__()
+
+        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):
+        super().__init__()
+
+        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/grltrackersource.py b/gnomemusic/grilowrappers/grltrackersource.py
index fcefcc2d..46e9b131 100644
--- a/gnomemusic/grilowrappers/grltrackersource.py
+++ b/gnomemusic/grilowrappers/grltrackersource.py
@@ -6,6 +6,7 @@ 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 GrlTrackerSource(GObject.GObject):
@@ -54,6 +55,8 @@ class GrlTrackerSource(GObject.GObject):
         self._initial_albums_fill(self._source)
         self._initial_artists_fill(self._source)
 
+        GrlTrackerPlaylists(source, coremodel, coreselection, grilo)
+
         self._source.connect("content-changed", self._on_content_changed)
 
     @GObject.Property(
diff --git a/gnomemusic/views/playlistsview.py b/gnomemusic/views/playlistsview.py
index 7a8c0747..b0eb4053 100644
--- a/gnomemusic/views/playlistsview.py
+++ b/gnomemusic/views/playlistsview.py
@@ -24,19 +24,16 @@
 
 from gettext import gettext as _
 
-from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
+from gi.repository import Gio, GObject, Gtk
 
 from gnomemusic import log
-from gnomemusic.grilo import grilo
-from gnomemusic.player import ValidationStatus, PlayerPlaylist
+from gnomemusic.player import PlayerPlaylist
 from gnomemusic.playlists import Playlists
 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.songwidget import SongWidget
 
 
 class PlaylistsView(BaseView):
@@ -59,46 +56,48 @@ class PlaylistsView(BaseView):
         super().__init__(
             'playlists', _("Playlists"), window, sidebar_container)
 
+        self._coremodel = window._app.props.coremodel
+        self._model = self._coremodel.props.playlists
         self._window = window
         self.player = player
 
-        self._view.get_style_context().add_class('songs-list')
+        # self._view.get_style_context().add_class('songs-list')
 
-        self._add_list_renderers()
+        # self._add_list_renderers()
 
         self._pl_ctrls = PlaylistControls()
         self._pl_ctrls.connect('playlist-renamed', self._on_playlist_renamed)
 
         self._song_popover = PlaylistContextMenu(self._view)
 
-        play_song = Gio.SimpleAction.new('play_song', None)
-        play_song.connect('activate', self._play_song)
-        self._window.add_action(play_song)
-
-        add_song_to_playlist = Gio.SimpleAction.new(
-            'add_song_to_playlist', None)
-        add_song_to_playlist.connect('activate', self._add_song_to_playlist)
-        self._window.add_action(add_song_to_playlist)
-
-        self._remove_song_action = Gio.SimpleAction.new('remove_song', None)
-        self._remove_song_action.connect(
-            'activate', self._stage_song_for_deletion)
-        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)
-        self._window.add_action(playlist_play_action)
-
-        self._playlist_delete_action = Gio.SimpleAction.new(
-            'playlist_delete', None)
-        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(
-            'activate', self._stage_playlist_for_renaming)
-        self._window.add_action(self._playlist_rename_action)
+        # play_song = Gio.SimpleAction.new('play_song', None)
+        # play_song.connect('activate', self._play_song)
+        # self._window.add_action(play_song)
+
+        # add_song_to_playlist = Gio.SimpleAction.new(
+        #     'add_song_to_playlist', None)
+        # add_song_to_playlist.connect('activate', self._add_song_to_playlist)
+        # self._window.add_action(add_song_to_playlist)
+
+        # self._remove_song_action = Gio.SimpleAction.new('remove_song', None)
+        # self._remove_song_action.connect(
+        #     'activate', self._stage_song_for_deletion)
+        # 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)
+        # self._window.add_action(playlist_play_action)
+
+        # self._playlist_delete_action = Gio.SimpleAction.new(
+        #     'playlist_delete', None)
+        # 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(
+        #     'activate', self._stage_playlist_for_renaming)
+        # self._window.add_action(self._playlist_rename_action)
 
         self._grid.insert_row(0)
         self._grid.attach(self._pl_ctrls, 1, 0, 1, 1)
@@ -118,25 +117,9 @@ class PlaylistsView(BaseView):
         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(
-            "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)
+            self._coremodel.props.playlists_sort,
+            self._add_playlist_to_sidebar)
 
         self.show_all()
 
@@ -150,134 +133,16 @@ class PlaylistsView(BaseView):
         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 = Gtk.ListBox()
 
-        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._controller = Gtk.GestureMultiPress().new(self._view)
-        self._controller.props.propagation_phase = Gtk.PropagationPhase.CAPTURE
-        self._controller.props.button = Gdk.BUTTON_SECONDARY
-        self._controller.connect("pressed", self._on_view_right_clicked)
+        # self._controller = Gtk.GestureMultiPress().new(self._view)
+        # self._controller.props.propagation_phase =
+        #    Gtk.PropagationPhase.CAPTURE
+        # self._controller.props.button = Gdk.BUTTON_SECONDARY
+        # self._controller.connect("pressed", self._on_view_right_clicked)
 
         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.position
-        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
@@ -295,229 +160,6 @@ class PlaylistsView(BaseView):
         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
-
-        rect = Gdk.Rectangle()
-        rect.x = x
-        rect.y = y - cell_y + 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)
-
-    @log
-    def _add_song_to_playlist(self, menuitem, data=None):
-        model, _iter = self._view.get_selection().get_selected()
-        song = model[_iter][5]
-
-        playlist_dialog = PlaylistDialog(self._window)
-        if playlist_dialog.run() == Gtk.ResponseType.ACCEPT:
-            self._playlists.add_to_playlist(
-                playlist_dialog.get_selected(), [song])
-        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)
-
-    @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")
-
-    @log
-    def _on_playlist_update(self, playlists, playlist):
-        """Refresh the displayed playlist if necessary
-
-        :param playlists: playlists object
-        :param Playlist playlist: updated playlist
-        """
-        if not self._is_current_playlist(playlist):
-            return
-
-        self._star_handler.star_renderer_click = False
-        for row in self._sidebar:
-            if playlist == row.playlist:
-                self._on_playlist_activated(self._sidebar, row)
-                break
-
-    @log
-    def _on_playlist_activation_request(self, klass, playlist):
-        """Selects and starts playing a playlist.
-
-        If the view has not been populated yet, populate it and then
-        select the requested playlist. Otherwise, directly select the
-        requested playlist and start playing.
-
-        :param Playlists klass: Playlists object
-        :param Playlist playlist: requested playlist
-        """
-        if not self._init:
-            self._plays_songs_on_activation = True
-            self._populate(playlist.props.pl_id)
-            return
-
-        playlist_row = None
-        for row in self._sidebar:
-            if row.playlist == playlist:
-                playlist_row = row
-                break
-
-        if not playlist_row:
-            return
-
-        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:
-            return
-        self._stage_playlist_for_deletion(None)
-
     @log
     def _on_playlist_activated(self, sidebar, row, data=None):
         """Update view with content from selected playlist"""
@@ -527,71 +169,30 @@ class PlaylistsView(BaseView):
         if self.rename_active:
             self._pl_ctrls.disable_rename_playlist()
 
+        self._view.bind_model(playlist.props.model, self._create_song_widget)
+
         self._current_playlist = playlist
         self._pl_ctrls.props.playlist_name = playlist_name
+        self._update_songs_count(playlist.props.count)
+        playlist.connect("notify::count", self._on_song_count_changed)
 
-        # 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.
+    def _on_song_count_changed(self, playlist, value):
+        self._update_songs_count(playlist.props.count)
 
-        Add all playlists found by Grilo to self._model
+    def _create_song_widget(self, coresong):
+        song_widget = SongWidget(coresong)
 
-        :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
+        song_widget.connect('button-release-event', self._song_activated)
 
-        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()])
+        return song_widget
 
-        self._update_songs_count(self._songs_count + 1)
-        return iter_
+    def _song_activated(self, widget, event):
+        self._coremodel.set_playlist_model(
+            PlayerPlaylist.Type.PLAYLIST, widget.props.coresong,
+            self._current_playlist.props.model)
+        self.player.play()
 
-    @log
-    def _on_play_activate(self, menuitem, data=None):
-        self._view.emit('row-activated', None, None)
+        return True
 
     @log
     def _is_current_playlist(self, playlist):
@@ -601,97 +202,6 @@ class PlaylistsView(BaseView):
 
         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))
-
-        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)
-
-    @log
-    def _undo_pending_deletion(self, playlist_notification):
-        """Revert the last playlist removal"""
-        notification_type = playlist_notification.type_
-
-        if notification_type == PlaylistNotification.Type.PLAYLIST:
-            pl_todelete = playlist_notification.data
-            self._playlists.undo_pending_deletion(pl_todelete)
-
-        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
-
-            iter_ = self._add_song_to_model(
-                song_todelete['song'], self.model, song_todelete['index'])
-
-            playlist_id = self._current_playlist.props.pl_id
-            if not self.player.playing_playlist(
-                    PlayerPlaylist.Type.PLAYLIST, playlist_id):
-                return
-
-            path = self.model.get_path(iter_)
-            self.player.add_song(self.model[iter_][5], int(path.to_string()))
-
-    @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)
-
     @GObject.Property(type=bool, default=False)
     def rename_active(self):
         """Indicate if renaming dialog is active"""
@@ -712,33 +222,6 @@ class PlaylistsView(BaseView):
         pl_torename.props.title = new_name
         self._playlists.rename(pl_torename, 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)
-
     @log
     def _populate(self, data=None):
         """Populate sidebar.


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