[gnome-music] Implement favourites playlist along with starring tracks



commit 1c66c4981969a191770ac931242b09676a1c84da
Author: Maia <maia mcc gmail com>
Date:   Fri Jan 23 17:54:38 2015 -0500

    Implement favourites playlist along with starring tracks
    
    'favorites' playlist added
    
    favorites are starred in 'songs' and 'playlists' views.
    
    (currently super hacky, storing favorite data as 'lyrics'. should update to storing in 'favourite' asap.)
    
    add/remove favorite queries
    
    view: make star renderer togglable
    
    view: ugly solution to skip on_item_activated if we clicked on start cellrenderer
    
    changed Tracker.SparqlConnection object to singleton (within TrackerWrapper)
    
    prelim. toggle_favorite func (not yet called)
    
    toggle favorite status in database when toggling star in Songs view
    
    added togglable stars to playlists view
    
    changed add/rm favorites to rely on song urls, not ids
    
    https://bugzilla.gnome.org/show_bug.cgi?id=743901

 gnomemusic/__init__.py  |   19 ++++++++
 gnomemusic/grilo.py     |   15 ++++++-
 gnomemusic/playlists.py |   16 +++++--
 gnomemusic/query.py     |   58 +++++++++++++++++++++++++-
 gnomemusic/view.py      |  106 +++++++++++++++++++++++++++++++++++++++--------
 gnomemusic/window.py    |   11 +----
 6 files changed, 192 insertions(+), 33 deletions(-)
---
diff --git a/gnomemusic/__init__.py b/gnomemusic/__init__.py
index db1f4e1..6a84a00 100644
--- a/gnomemusic/__init__.py
+++ b/gnomemusic/__init__.py
@@ -25,6 +25,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 gi.repository import Tracker
 import logging
 logger = logging.getLogger(__name__)
 tabbing = 0
@@ -47,3 +48,21 @@ def log(fn):
 
         return retval
     return wrapped
+
+class TrackerWrapper:
+    class __TrackerWrapper:
+        def __init__(self):
+            try:
+                self.tracker = Tracker.SparqlConnection.get(None)
+            except Exception as e:
+                from sys import exit
+                logger.error("Cannot connect to tracker, error '%s'\Exiting" % str(e))
+                exit(1)
+        def __str__(self):
+            return repr(self)
+    instance = None
+    def __init__(self):
+        if not TrackerWrapper.instance:
+            TrackerWrapper.instance = TrackerWrapper.__TrackerWrapper()
+    def __getattr__(self, name):
+        return getattr(self.instance, name)
\ No newline at end of file
diff --git a/gnomemusic/grilo.py b/gnomemusic/grilo.py
index be2bd86..cf09788 100644
--- a/gnomemusic/grilo.py
+++ b/gnomemusic/grilo.py
@@ -26,7 +26,7 @@
 # delete this exception statement from your version.
 from gi.repository import GLib, GObject
 from gnomemusic.query import Query
-from gnomemusic import log
+from gnomemusic import log, TrackerWrapper
 import logging
 import os
 os.environ['GRL_PLUGIN_RANKS'] = 'local-metadata:3,filesystem:2,tracker:1,lastfm-albumart:0'
@@ -82,6 +82,8 @@ class Grilo(GObject.GObject):
 
         self.registry = Grl.Registry.get_default()
 
+        self.sparqltracker = TrackerWrapper().tracker
+
     @log
     def _find_sources(self):
         self.registry.connect('source_added', self._on_source_added)
@@ -216,6 +218,17 @@ class Grilo(GObject.GObject):
         self.tracker.query(query, self.METADATA_KEYS, options, _callback, data)
 
     @log
+    def toggle_favorite(self, song_item):
+        # TODO: change "bool(song_item.get_lyrics())" --> song_item.get_favourite() once query works properly
+        # TODO: when .set/get_favourite work, set_favourite outside loop: 
item.set_favourite(!item.get_favourite())
+        if bool(song_item.get_lyrics()): # is favorite
+            self.sparqltracker.update(Query.remove_favorite(song_item.get_url()), GLib.PRIORITY_DEFAULT, 
None)
+            song_item.set_lyrics("")
+        else: # not favorite
+            self.sparqltracker.update(Query.add_favorite(song_item.get_url()), GLib.PRIORITY_DEFAULT, None)
+            song_item.set_lyrics("i'm truthy")
+
+    @log
     def search(self, q, callback, data=None):
         options = self.options.copy()
 
diff --git a/gnomemusic/playlists.py b/gnomemusic/playlists.py
index 1eedb3f..f713b2e 100644
--- a/gnomemusic/playlists.py
+++ b/gnomemusic/playlists.py
@@ -27,6 +27,7 @@
 
 
 from gi.repository import Grl, GLib, GObject
+from gnomemusic import TrackerWrapper
 from gnomemusic.grilo import grilo
 from gnomemusic.query import Query
 from gettext import gettext as _
@@ -68,6 +69,13 @@ class StaticPlaylists:
         # TRANSLATORS: this is a playlist name
         TITLE = _("Recently Added")
 
+    class Favorites:
+        ID = None
+        QUERY = Query.get_favorite_songs()
+        TAG_TEXT = "FAVORITES"
+        # TRANSLATORS: this is a playlist name
+        TITLE = _("Favorite Songs")
+
 
 class Playlists(GObject.GObject):
     __gsignals__ = {
@@ -87,17 +95,15 @@ class Playlists(GObject.GObject):
     @classmethod
     def get_default(self, tracker=None):
         if self.instance:
-            if not self.tracker and tracker:
-                self.instance.tracker = tracker
             return self.instance
         else:
-            self.instance = Playlists(tracker)
+            self.instance = Playlists()
         return self.instance
 
     @log
-    def __init__(self, tracker):
+    def __init__(self):
         GObject.GObject.__init__(self)
-        self.tracker = tracker
+        self.tracker = TrackerWrapper().tracker
 
     @log
     def fetch_or_create_static_playlists(self):
diff --git a/gnomemusic/query.py b/gnomemusic/query.py
index ac806c5..9bfc65d 100644
--- a/gnomemusic/query.py
+++ b/gnomemusic/query.py
@@ -425,8 +425,13 @@ class Query():
         nmm:artistName(nmm:performer(?song)) AS artist
         nie:title(nmm:musicAlbum(?song)) AS album
         nfo:duration(?song) AS duration
+        IF(bound(?tag), 'truth!', '') AS lyrics
         {
             %(where_clause)s
+            OPTIONAL {
+                ?song nao:hasTag ?tag .
+                FILTER( ?tag = nao:predefined-tag-favorite )
+            }
             FILTER (
                 tracker:uri-is-descendant(
                     '%(music_dir)s', nie:url(?song)
@@ -547,6 +552,7 @@ class Query():
         nmm:artistName(nmm:performer(?song)) AS artist
         nie:title(nmm:musicAlbum(?song)) AS album
         nfo:duration(?song) AS duration
+        IF(bound(?tag), 'truth!', '') AS lyrics
     WHERE {
         ?playlist a nmm:Playlist ;
             a nfo:MediaList ;
@@ -556,6 +562,10 @@ class Query():
         ?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
         )
@@ -967,7 +977,7 @@ class Query():
                 nie:isStoredAs ?as .
           ?as nie:url ?url .
         } ORDER BY DESC(?count) LIMIT 50
-        """
+        """.replace('\n', ' ').strip()
 
         return query
 
@@ -1011,7 +1021,7 @@ class Query():
     def get_recently_added_songs():
         #TODO: or this could take comparison date as an argument so we don't need to make a date string in 
query.py...
         #TODO: set time interval somewhere? A settings file? (Default is maybe 2 weeks...?)
-        
+
         days_difference = 7 # currently hardcoding time interval of 7 days
         seconds_difference = days_difference * SECONDS_PER_DAY
         compare_date = time.strftime(sparql_midnight_dateTime_format, 
time.gmtime(time.time()-seconds_difference))
@@ -1029,6 +1039,19 @@ class Query():
 
         return query
 
+    def get_favorite_songs():
+        query = """
+    SELECT ?url
+    WHERE {
+        ?song a nmm:MusicPiece ;
+            nie:isStoredAs ?as ;
+            nao:hasTag nao:predefined-tag-favorite .
+        ?as nie:url ?url .
+    } ORDER BY DESC(tracker:added(?song))
+    """.replace('\n', ' ').strip()
+
+        return query
+
     # Functions for search
     # TODO: make those queries actually return something
     @staticmethod
@@ -1290,4 +1313,35 @@ class Query():
             'playlist_id': playlist_id
         }
 
+        return query
+
+    def add_favorite(song_url):
+        query = """
+            INSERT {
+                ?song nao:hasTag nao:predefined-tag-favorite
+            }
+            WHERE {
+                ?song a nmm:MusicPiece .
+                FILTER ( nie:url(?song) = "%(song_url)s" )
+            }
+        """.replace("\n", " ").strip() % {
+            'song_url': song_url
+
+        }
+
+        return query
+
+    def remove_favorite(song_url):
+        query = """
+            DELETE {
+                ?song nao:hasTag nao:predefined-tag-favorite
+            }
+            WHERE {
+                ?song a nmm:MusicPiece .
+                FILTER ( nie:url(?song) = "%(song_url)s" )
+            }
+        """.replace("\n", " ").strip() % {
+            'song_url': song_url
+        }
+
         return query
\ No newline at end of file
diff --git a/gnomemusic/view.py b/gnomemusic/view.py
index b4ce329..ee7e445 100644
--- a/gnomemusic/view.py
+++ b/gnomemusic/view.py
@@ -44,7 +44,7 @@ from gnomemusic.grilo import grilo
 from gnomemusic.query import Query
 from gnomemusic.toolbar import ToolbarState
 import gnomemusic.widgets as Widgets
-from gnomemusic.playlists import Playlists
+from gnomemusic.playlists import Playlists, StaticPlaylists
 from gnomemusic.albumArtCache import AlbumArtCache as albumArtCache
 from gnomemusic import log
 import logging
@@ -56,7 +56,6 @@ playlists = Playlists.get_default()
 class ViewContainer(Gtk.Stack):
     nowPlayingIconName = 'media-playback-start-symbolic'
     errorIconName = 'dialog-error-symbolic'
-    starIconName = 'starred-symbolic'
 
     @log
     def __init__(self, name, title, window, view_type, use_sidebar=False, sidebar=None):
@@ -105,7 +104,8 @@ class ViewContainer(Gtk.Stack):
         if not use_sidebar or sidebar:
             self._grid.add(box)
 
-        self.view.connect('item-activated', self._on_item_activated)
+        self.view.click_handler = self.view.connect('item-activated', self._on_item_activated)
+        self.star_renderer_click = False
         self.view.connect('selection-mode-request', self._on_selection_mode_request)
         self._cursor = None
         self.window = window
@@ -267,6 +267,23 @@ class ViewContainer(Gtk.Stack):
     def get_selected_tracks(self, callback):
         callback([])
 
+    @log
+    def _on_star_toggled(self, widget, path):
+        print("_on_star_toggled")
+        try:
+            _iter = self._model.get_iter(path)
+        except TypeError:
+            return
+
+        new_value = not self._model.get_value(_iter, 9)
+        self._model.set_value(_iter, 9, new_value)
+        song_item = self._model.get_value(_iter, 5) # er, will this definitely return MediaAudio obj.?
+        grilo.toggle_favorite(song_item) # toggle favorite status in database
+        playlists.update_static_playlist(StaticPlaylists.Favorites)
+
+        # Use this flag to ignore the upcoming _on_item_activated call
+        self.star_renderer_click = True
+
 
 # Class for the Empty View
 class Empty(Gtk.Stack):
@@ -311,6 +328,10 @@ class Albums(ViewContainer):
 
     @log
     def _on_item_activated(self, widget, id, path):
+        if self.star_renderer_click:
+            self.star_renderer_click = False
+            return
+
         try:
             _iter = self._model.get_iter(path)
         except TypeError:
@@ -366,6 +387,41 @@ class Albums(ViewContainer):
                 self.items_selected_callback(self.items_selected)
 
 
+class CellRendererClickablePixbuf(Gtk.CellRendererPixbuf):
+
+    __gsignals__ = {'clicked': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
+                                (GObject.TYPE_STRING,))}
+    __gproperties__ = {
+        'show_star': (GObject.TYPE_BOOLEAN, 'Show star', 'show star', False, GObject.PARAM_READWRITE)}
+
+    starIcon = 'starred-symbolic'
+    nonStarIcon = 'non-starred-symbolic'
+
+    def __init__(self, view, *args, **kwargs):
+        Gtk.CellRendererPixbuf.__init__(self, *args, **kwargs)
+        self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE)
+        self.set_property('xpad', 32)
+        self.set_property('icon_name', self.nonStarIcon)
+        self.view = view
+        self.show_star = False
+
+    def do_activate(self, event, widget, path, background_area, cell_area, flags):
+        self.show_star = False
+        self.emit('clicked', path)
+
+    def do_get_property(self, property):
+        if property.name == 'show-star':
+            return self.show_star
+
+    def do_set_property(self, property, value):
+        if property.name == 'show-star':
+            if self.show_star:
+                self.set_property('icon_name', self.starIcon)
+            else:
+                self.set_property('icon_name', self.nonStarIcon)
+            self.show_star = value
+
+
 class Songs(ViewContainer):
     @log
     def __init__(self, window, player):
@@ -396,6 +452,10 @@ class Songs(ViewContainer):
 
     @log
     def _on_item_activated(self, widget, id, path):
+        if self.star_renderer_click:
+            self.star_renderer_click = False
+            return
+
         try:
             _iter = self._model.get_iter(path)
         except TypeError:
@@ -434,7 +494,8 @@ class Songs(ViewContainer):
             -1,
             [2, 3, 5, 8, 9, 10],
             [albumArtCache.get_media_title(item),
-             artist, item, self.nowPlayingIconName, False, False])
+             artist, item, self.nowPlayingIconName, bool(item.get_lyrics()), False])
+        # TODO: change "bool(item.get_lyrics())" --> item.get_favourite() once query works properly
         self.player.discover_item(item, self._on_discovered, _iter)
 
     @log
@@ -465,13 +526,12 @@ class Songs(ViewContainer):
                                  self._on_list_widget_title_render, None)
         cols[0].add_attribute(title_renderer, 'text', 2)
 
-        star_renderer = Gtk.CellRendererPixbuf(
-            xpad=32,
-            icon_name=self.starIconName
-        )
+        # ADD STAR RENDERERS
+        star_renderer = CellRendererClickablePixbuf(self.view)
+        star_renderer.connect("clicked", self._on_star_toggled)
         list_widget.add_renderer(star_renderer,
-                                 self._on_list_widget_star_render, None)
-        cols[0].add_attribute(star_renderer, 'visible', 9)
+                                self._on_list_widget_star_render, None)
+        cols[0].add_attribute(star_renderer, 'show_star', 9)
 
         duration_renderer = Gd.StyledTextRenderer(
             xpad=32,
@@ -615,6 +675,10 @@ class Artists (ViewContainer):
 
     @log
     def _on_item_activated(self, widget, item_id, path):
+        if self.star_renderer_click:
+            self.star_renderer_click = False
+            return
+
         try:
             _iter = self._model.get_iter(path)
         except TypeError:
@@ -856,13 +920,12 @@ class Playlist(ViewContainer):
                                  self._on_list_widget_title_render, None)
         cols[0].add_attribute(title_renderer, 'text', 2)
 
-        star_renderer = Gtk.CellRendererPixbuf(
-            xpad=32,
-            icon_name=self.starIconName
-        )
+        # ADD STAR RENDERERS
+        star_renderer = CellRendererClickablePixbuf(self.view)
+        star_renderer.connect("clicked", self._on_star_toggled)
         list_widget.add_renderer(star_renderer,
-                                 self._on_list_widget_star_render, None)
-        cols[0].add_attribute(star_renderer, 'visible', 9)
+                                self._on_list_widget_star_render, None)
+        cols[0].add_attribute(star_renderer, 'show_star', 9)
 
         duration_renderer = Gd.StyledTextRenderer(
             xpad=32,
@@ -970,6 +1033,10 @@ class Playlist(ViewContainer):
 
     @log
     def _on_item_activated(self, widget, id, path):
+        if self.star_renderer_click:
+            self.star_renderer_click = False
+            return
+
         try:
             _iter = self._model.get_iter(path)
         except TypeError:
@@ -1097,7 +1164,8 @@ class Playlist(ViewContainer):
         _iter = model.insert_with_valuesv(
             -1,
             [2, 3, 5, 8, 9, 10],
-            [title, artist, item, self.nowPlayingIconName, False, False])
+            [title, artist, item, self.nowPlayingIconName, bool(item.get_lyrics()), False])
+        # TODO: change "bool(item.get_lyrics())" --> item.get_favourite() once query works properly
         self.player.discover_item(item, self._on_discovered, _iter)
         self.songs_count += 1
         self._update_songs_count()
@@ -1274,6 +1342,10 @@ class Search(ViewContainer):
 
     @log
     def _on_item_activated(self, widget, id, path):
+        if self.star_renderer_click:
+            self.star_renderer_click = False
+            return
+
         try:
             child_path = self.filter_model.convert_path_to_child_path(path)
         except TypeError:
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index b84f037..c0ed056 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -34,6 +34,7 @@ from gi.repository import Gtk, Gdk, Gio, GLib, Tracker
 from gi.repository import Gd
 from gettext import gettext as _, ngettext
 
+from gnomemusic import TrackerWrapper
 from gnomemusic.toolbar import Toolbar, ToolbarState
 from gnomemusic.player import Player, SelectionToolbar
 from gnomemusic.query import Query
@@ -45,14 +46,8 @@ from gnomemusic import log
 import logging
 logger = logging.getLogger(__name__)
 
-try:
-    tracker = Tracker.SparqlConnection.get(None)
-except Exception as e:
-    from sys import exit
-    logger.error("Cannot connect to tracker, error '%s'\Exiting" % str(e))
-    exit(1)
-playlist = Playlists.get_default(tracker)
-
+tracker = TrackerWrapper().tracker
+playlist = Playlists.get_default()
 
 class Window(Gtk.ApplicationWindow):
 


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