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