[gnome-music] Implement favourites playlist along with starring tracks
- From: Vadim Rutkovsky <vrutkovsky src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-music] Implement favourites playlist along with starring tracks
- Date: Tue, 10 Feb 2015 11:25:47 +0000 (UTC)
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]