[gnome-music/wip/mschraal/core: 188/190] playlistview: Very basic functional
- From: Marinus Schraal <mschraal src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-music/wip/mschraal/core: 188/190] playlistview: Very basic functional
- Date: Sun, 7 Jul 2019 21:49:14 +0000 (UTC)
commit 08ec8a7af25ec423a30ca4b5df087aa42249d423
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 | 562 ++----------------------
4 files changed, 547 insertions(+), 531 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..fdcf2208 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,12 +56,14 @@ 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)
@@ -134,7 +133,8 @@ class PlaylistsView(BaseView):
self._playlists_model = self._playlists.get_playlists_model()
self._sidebar.bind_model(
- self._playlists_model, self._add_playlist_to_sidebar)
+ self._coremodel.props.playlists_sort,
+ self._add_playlist_to_sidebar)
self._playlists_model.connect(
"items-changed", self._on_playlists_model_changed)
@@ -150,134 +150,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 +177,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 +186,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)
+ def _on_song_count_changed(self, playlist, value):
+ self._update_songs_count(playlist.props.count)
- 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)
+ def _create_song_widget(self, coresong):
+ song_widget = SongWidget(coresong)
- @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
+ song_widget.connect('button-release-event', self._song_activated)
- @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
+ return song_widget
- 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()])
+ 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()
- 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)
+ return True
@log
def _is_current_playlist(self, playlist):
@@ -601,97 +219,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 +239,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]