From d40c9ac84401d8687164a665a60f5784cbb7d44e Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 14:33:01 +0200 Subject: [PATCH 01/27] Split queue into own class and add stack to swtich between library and queue --- src/MainWindow.vala | 166 +++++++------------------------------ src/Views/LibraryView.vala | 5 ++ src/Views/QueueView.vala | 165 ++++++++++++++++++++++++++++++++++++ src/meson.build | 2 + 4 files changed, 202 insertions(+), 136 deletions(-) create mode 100644 src/Views/LibraryView.vala create mode 100644 src/Views/QueueView.vala diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 01a51903e..a8cd9ee08 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -4,66 +4,41 @@ */ public class Music.MainWindow : Gtk.ApplicationWindow { - private Gtk.Button repeat_button; - private Gtk.Button shuffle_button; + private Gtk.Button back_button; + private QueueView queue_view; + private Gtk.Stack stack; private Settings settings; construct { - var playback_manager = PlaybackManager.get_default (); - var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); - shuffle_button = new Gtk.Button.from_icon_name ("media-playlist-shuffle-symbolic") { - action_name = Application.ACTION_PREFIX + Application.ACTION_SHUFFLE, - tooltip_text = _("Shuffle") - }; + back_button = new Gtk.Button.with_label (_("Library")); + back_button.add_css_class (Granite.STYLE_CLASS_BACK_BUTTON); - repeat_button = new Gtk.Button (); + var queue_button = new Gtk.Button.from_icon_name ("view-list-symbolic"); - var queue_header = new Gtk.HeaderBar () { + var start_header = new Gtk.HeaderBar () { show_title_buttons = false, title_widget = new Gtk.Label ("") }; - queue_header.add_css_class (Granite.STYLE_CLASS_FLAT); - queue_header.add_css_class (Granite.STYLE_CLASS_DEFAULT_DECORATION); - queue_header.pack_start (start_window_controls); - queue_header.pack_end (shuffle_button); - queue_header.pack_end (repeat_button); - - var queue_placeholder = new Granite.Placeholder (_("Queue is Empty")) { - description = _("Audio files opened from Files will appear here"), - icon = new ThemedIcon ("playlist-queue") - }; + start_header.add_css_class (Granite.STYLE_CLASS_FLAT); + start_header.pack_start (start_window_controls); + start_header.pack_start (back_button); + start_header.pack_end (queue_button); - var queue_listbox = new Gtk.ListBox () { - hexpand = true, - vexpand = true - }; - queue_listbox.bind_model (playback_manager.queue_liststore, create_queue_row); - queue_listbox.set_placeholder (queue_placeholder); + queue_view = new QueueView (); - var scrolled = new Gtk.ScrolledWindow () { - child = queue_listbox - }; - - var drop_target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); + var library_view = new LibraryView (); - var queue = new Gtk.Grid (); - queue.add_css_class (Granite.STYLE_CLASS_VIEW); - queue.attach (queue_header, 0, 0); - queue.attach (scrolled, 0, 1); - queue.add_controller (drop_target); + stack = new Gtk.Stack (); + stack.add_child (queue_view); + stack.add_child (library_view); + stack.visible_child = library_view; - var error_toast = new Granite.Toast (""); - - var queue_overlay = new Gtk.Overlay () { - child = queue - }; - queue_overlay.add_overlay (error_toast); - - var queue_handle = new Gtk.WindowHandle () { - child = queue_overlay - }; + var start_box = new Gtk.Box (VERTICAL, 0); + start_box.add_css_class (Granite.STYLE_CLASS_VIEW); + start_box.append (start_header); + start_box.append (stack); var end_window_controls = new Gtk.WindowControls (Gtk.PackType.END); @@ -72,7 +47,6 @@ public class Music.MainWindow : Gtk.ApplicationWindow { title_widget = new Gtk.Label ("") }; end_header.add_css_class (Granite.STYLE_CLASS_FLAT); - end_header.add_css_class (Granite.STYLE_CLASS_DEFAULT_DECORATION); end_header.pack_end (end_window_controls); var now_playing_view = new NowPlayingView () { @@ -92,7 +66,7 @@ public class Music.MainWindow : Gtk.ApplicationWindow { }; var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL) { - start_child = queue_handle, + start_child = start_box, end_child = now_playing_handle, resize_end_child = false, shrink_end_child = false, @@ -109,100 +83,20 @@ public class Music.MainWindow : Gtk.ApplicationWindow { settings = new Settings ("io.elementary.music"); settings.bind ("pane-position", paned, "position", SettingsBindFlags.DEFAULT); - settings.changed["repeat-mode"].connect (update_repeat_button); - - update_repeat_button (); - - drop_target.drop.connect ((target, value, x, y) => { - if (value.type () == typeof (Gdk.FileList)) { - File[] files; - SList file_list = null; - foreach (unowned var file in (SList) value.get_boxed ()) { - var file_type = file.query_file_type (FileQueryInfoFlags.NONE); - if (file_type == FileType.DIRECTORY) { - prepend_directory_files (file, ref file_list); - } else { - file_list.prepend (file); - } - } - - file_list.reverse (); - foreach (unowned var file in file_list) { - files += file; - } - - playback_manager.queue_files (files); - - return true; - } - - return false; - }); - playback_manager.invalids_found.connect ((count) => { - error_toast.title = ngettext ( - "%d invalid file was not added to the queue", - "%d invalid files were not added to the queue", - count).printf (count); - error_toast.send_notification (); - }); + update_header_buttons (); + stack.notify["visible-child"].connect (update_header_buttons); - repeat_button.clicked.connect (() => { - var enum_step = settings.get_enum ("repeat-mode"); - if (enum_step < 2) { - settings.set_enum ("repeat-mode", enum_step + 1); - } else { - settings.set_enum ("repeat-mode", 0); - } + queue_button.clicked.connect (() => { + stack.visible_child = queue_view; }); - queue_listbox.row_activated.connect ((row) => { - playback_manager.current_audio = ((TrackRow) row).audio_object; + back_button.clicked.connect (() => { + stack.visible_child = library_view; }); } - //Array concatenation not permitted for parameters so use a list instead - private void prepend_directory_files (GLib.File dir, ref SList file_list) { - try { - var enumerator = dir.enumerate_children ( - "standard::*", - FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - null - ); - - FileInfo info = null; - while ((info = enumerator.next_file (null)) != null) { - var child = dir.resolve_relative_path (info.get_name ()); - if (info.get_file_type () == FileType.DIRECTORY) { - prepend_directory_files (child, ref file_list); - } else { - file_list.prepend (child); - } - } - } catch (Error e) { - warning ("Error while enumerating children of %s: %s", dir.get_uri (), e.message); - } - } - - private void update_repeat_button () { - switch (settings.get_string ("repeat-mode")) { - case "disabled": - repeat_button.icon_name = "media-playlist-no-repeat-symbolic"; - repeat_button.tooltip_text = _("Repeat None"); - break; - case "all": - repeat_button.icon_name = "media-playlist-repeat-symbolic"; - repeat_button.tooltip_text = _("Repeat All"); - break; - case "one": - repeat_button.icon_name = "media-playlist-repeat-song-symbolic"; - repeat_button.tooltip_text = _("Repeat One"); - break; - } - } - - private Gtk.Widget create_queue_row (GLib.Object object) { - unowned var audio_object = (AudioObject) object; - return new TrackRow (audio_object); + private void update_header_buttons () { + back_button.visible = stack.visible_child == queue_view; } } diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala new file mode 100644 index 000000000..a1dd827c5 --- /dev/null +++ b/src/Views/LibraryView.vala @@ -0,0 +1,5 @@ +public class LibraryView : Gtk.Box { + construct { + append (new Gtk.Label ("test")); + } +} diff --git a/src/Views/QueueView.vala b/src/Views/QueueView.vala new file mode 100644 index 000000000..993639556 --- /dev/null +++ b/src/Views/QueueView.vala @@ -0,0 +1,165 @@ +public class Music.QueueView : Gtk.Box { + private Gtk.Button repeat_button; + private Gtk.Button shuffle_button; + private Settings settings; + + construct { + var playback_manager = PlaybackManager.get_default (); + + var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); + + shuffle_button = new Gtk.Button.from_icon_name ("media-playlist-shuffle-symbolic") { + action_name = Application.ACTION_PREFIX + Application.ACTION_SHUFFLE, + tooltip_text = _("Shuffle") + }; + + repeat_button = new Gtk.Button (); + + var queue_header = new Gtk.HeaderBar () { + show_title_buttons = false, + title_widget = new Gtk.Label ("") + }; + queue_header.add_css_class (Granite.STYLE_CLASS_FLAT); + queue_header.add_css_class (Granite.STYLE_CLASS_DEFAULT_DECORATION); + queue_header.pack_start (start_window_controls); + queue_header.pack_end (shuffle_button); + queue_header.pack_end (repeat_button); + + var queue_placeholder = new Granite.Placeholder (_("Queue is Empty")) { + description = _("Audio files opened from Files will appear here"), + icon = new ThemedIcon ("playlist-queue") + }; + + var queue_listbox = new Gtk.ListBox () { + hexpand = true, + vexpand = true + }; + queue_listbox.bind_model (playback_manager.queue_liststore, create_queue_row); + queue_listbox.set_placeholder (queue_placeholder); + + var scrolled = new Gtk.ScrolledWindow () { + child = queue_listbox + }; + + var drop_target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); + + var queue = new Gtk.Grid (); + // queue.attach (queue_header, 0, 0); + queue.attach (scrolled, 0, 1); + queue.add_controller (drop_target); + + var error_toast = new Granite.Toast (""); + + var queue_overlay = new Gtk.Overlay () { + child = queue + }; + queue_overlay.add_overlay (error_toast); + + var queue_handle = new Gtk.WindowHandle () { + child = queue_overlay, + hexpand = true + }; + + hexpand = true; + vexpand = true; + append (queue_handle); + + settings = new Settings ("io.elementary.music"); + settings.changed["repeat-mode"].connect (update_repeat_button); + + update_repeat_button (); + + drop_target.drop.connect ((target, value, x, y) => { + if (value.type () == typeof (Gdk.FileList)) { + File[] files; + SList file_list = null; + foreach (unowned var file in (SList) value.get_boxed ()) { + var file_type = file.query_file_type (FileQueryInfoFlags.NONE); + if (file_type == FileType.DIRECTORY) { + prepend_directory_files (file, ref file_list); + } else { + file_list.prepend (file); + } + } + + file_list.reverse (); + foreach (unowned var file in file_list) { + files += file; + } + + playback_manager.queue_files (files); + + return true; + } + + return false; + }); + + playback_manager.invalids_found.connect ((count) => { + error_toast.title = ngettext ( + "%d invalid file was not added to the queue", + "%d invalid files were not added to the queue", + count).printf (count); + error_toast.send_notification (); + }); + + repeat_button.clicked.connect (() => { + var enum_step = settings.get_enum ("repeat-mode"); + if (enum_step < 2) { + settings.set_enum ("repeat-mode", enum_step + 1); + } else { + settings.set_enum ("repeat-mode", 0); + } + }); + + queue_listbox.row_activated.connect ((row) => { + playback_manager.current_audio = ((TrackRow) row).audio_object; + }); + } + + //Array concatenation not permitted for parameters so use a list instead + private void prepend_directory_files (GLib.File dir, ref SList file_list) { + try { + var enumerator = dir.enumerate_children ( + "standard::*", + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + + FileInfo info = null; + while ((info = enumerator.next_file (null)) != null) { + var child = dir.resolve_relative_path (info.get_name ()); + if (info.get_file_type () == FileType.DIRECTORY) { + prepend_directory_files (child, ref file_list); + } else { + file_list.prepend (child); + } + } + } catch (Error e) { + warning ("Error while enumerating children of %s: %s", dir.get_uri (), e.message); + } + } + + private void update_repeat_button () { + switch (settings.get_string ("repeat-mode")) { + case "disabled": + repeat_button.icon_name = "media-playlist-no-repeat-symbolic"; + repeat_button.tooltip_text = _("Repeat None"); + break; + case "all": + repeat_button.icon_name = "media-playlist-repeat-symbolic"; + repeat_button.tooltip_text = _("Repeat All"); + break; + case "one": + repeat_button.icon_name = "media-playlist-repeat-song-symbolic"; + repeat_button.tooltip_text = _("Repeat One"); + break; + } + } + + private Gtk.Widget create_queue_row (GLib.Object object) { + unowned var audio_object = (AudioObject) object; + return new TrackRow (audio_object); + } + +} diff --git a/src/meson.build b/src/meson.build index e4b85f439..9e4372148 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,7 +5,9 @@ sources = [ 'PlaybackManager.vala', 'DBus/MprisPlayer.vala', 'DBus/MprisRoot.vala', + 'Views/LibraryView.vala', 'Views/NowPlayingView.vala', + 'Views/QueueView.vala', 'Widgets/AlbumImage.vala', 'Widgets/SeekBar.vala', 'Widgets/TrackRow.vala', From add684b651a8f9af6b5e0bafd014dbca741a42b9 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 15:07:13 +0200 Subject: [PATCH 02/27] Add library prototype --- src/Views/LibraryView.vala | 30 ++++++++++++++++++++++++++++-- src/Views/QueueView.vala | 4 +++- src/Widgets/TrackRow.vala | 35 ++++++++++++++++++++--------------- src/meson.build | 1 + 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index a1dd827c5..1d77624e7 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -1,5 +1,31 @@ -public class LibraryView : Gtk.Box { +public class Music.LibraryView : Gtk.Box { construct { - append (new Gtk.Label ("test")); + var library_manager = LibraryManager.get_instance (); + + var selection_model = new Gtk.SingleSelection (library_manager.songs); + + var factory = new Gtk.SignalListItemFactory (); + factory.setup.connect (setup_widget); + factory.bind.connect (bind_item); + + var list_view = new Gtk.ListView (selection_model, factory) { + hexpand = true + }; + + append (list_view); + } + + private void setup_widget (Object obj) { + var list_item = (Gtk.ListItem) obj; + + list_item.child = new TrackRow (); + } + + private void bind_item (Object obj) { + var list_item = (Gtk.ListItem) obj; + + var audio_object = (AudioObject)list_item.item; + + ((TrackRow)list_item.child).bind_audio_object (audio_object); } } diff --git a/src/Views/QueueView.vala b/src/Views/QueueView.vala index 993639556..14a8aee0f 100644 --- a/src/Views/QueueView.vala +++ b/src/Views/QueueView.vala @@ -159,7 +159,9 @@ public class Music.QueueView : Gtk.Box { private Gtk.Widget create_queue_row (GLib.Object object) { unowned var audio_object = (AudioObject) object; - return new TrackRow (audio_object); + var track_row = new TrackRow (); + track_row.bind_audio_object (audio_object); + return track_row; } } diff --git a/src/Widgets/TrackRow.vala b/src/Widgets/TrackRow.vala index df929364a..90b7f8e5e 100644 --- a/src/Widgets/TrackRow.vala +++ b/src/Widgets/TrackRow.vala @@ -3,18 +3,17 @@ * SPDX-FileCopyrightText: 2021 elementary, Inc. (https://elementary.io) */ -public class Music.TrackRow : Gtk.ListBoxRow { - public AudioObject audio_object { get; construct; } +public class Music.TrackRow : Gtk.Box { + public AudioObject audio_object { get; private set; } private static Gtk.CssProvider css_provider; private static PlaybackManager playback_manager; + private Gtk.Label title_label; + private Gtk.Label artist_label; + private AlbumImage album_image; private Gtk.Spinner play_icon; - public TrackRow (AudioObject audio_object) { - Object (audio_object: audio_object); - } - static construct { playback_manager = PlaybackManager.get_default (); @@ -28,17 +27,17 @@ public class Music.TrackRow : Gtk.ListBoxRow { }; play_icon.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - var album_image = new Music.AlbumImage (); + album_image = new Music.AlbumImage (); album_image.image.height_request = 32; album_image.image.width_request = 32; - var title_label = new Gtk.Label (audio_object.title) { + title_label = new Gtk.Label ("") { ellipsize = Pango.EllipsizeMode.MIDDLE, hexpand = true, xalign = 0 }; - var artist_label = new Gtk.Label (audio_object.artist) { + artist_label = new Gtk.Label ("") { ellipsize = Pango.EllipsizeMode.MIDDLE, hexpand = true, xalign = 0 @@ -58,14 +57,10 @@ public class Music.TrackRow : Gtk.ListBoxRow { grid.attach (artist_label, 1, 1); grid.attach (play_icon, 2, 0, 1, 2); - child = grid; - - audio_object.bind_property ("artist", artist_label, "label", BindingFlags.SYNC_CREATE); - audio_object.bind_property ("title", title_label, "label", BindingFlags.SYNC_CREATE); - audio_object.bind_property ("texture", album_image.image, "paintable", BindingFlags.SYNC_CREATE); + append (grid); playback_manager.notify["current-audio"].connect (() => { - play_icon.spinning = playback_manager.current_audio == audio_object; + play_icon.spinning = audio_object != null ? playback_manager.current_audio == audio_object : false; }); var play_pause_action = (SimpleAction) GLib.Application.get_default ().lookup_action (Application.ACTION_PLAY_PAUSE); @@ -79,6 +74,16 @@ public class Music.TrackRow : Gtk.ListBoxRow { } + public void bind_audio_object (AudioObject audio_object) { + this.audio_object = audio_object; + + title_label.label = audio_object.title; + artist_label.label = audio_object.artist; + album_image.image.paintable = audio_object.texture; + + play_icon.spinning = playback_manager.current_audio == audio_object; + } + private void update_playing (bool playing) { if (playing) { play_icon.add_css_class ("playing"); diff --git a/src/meson.build b/src/meson.build index 9e4372148..a8cee7830 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ sources = [ 'PlaybackManager.vala', 'DBus/MprisPlayer.vala', 'DBus/MprisRoot.vala', + 'Services/LibraryManager.vala', 'Views/LibraryView.vala', 'Views/NowPlayingView.vala', 'Views/QueueView.vala', From 87fedc09b606dd281c1e6a1b825a61b68ed1f8e6 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 15:13:45 +0200 Subject: [PATCH 03/27] Playback from library is now working --- src/Services/LibraryManager.vala | 44 ++++++++++++++++++++++++++++++++ src/Views/LibraryView.vala | 6 +++++ 2 files changed, 50 insertions(+) create mode 100644 src/Services/LibraryManager.vala diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala new file mode 100644 index 000000000..4ff3c1273 --- /dev/null +++ b/src/Services/LibraryManager.vala @@ -0,0 +1,44 @@ +public class Music.LibraryManager : Object { + public ListStore songs { get; construct; } + + private static GLib.Once instance; + public static unowned LibraryManager get_instance () { + return instance.once (() => { return new LibraryManager (); }); + } + + construct { + songs = new ListStore (typeof (AudioObject)); + detect_audio_files.begin (); + } + + public async void detect_audio_files () throws Error { + File directory = File.new_for_path (Environment.get_user_special_dir (UserDirectory.MUSIC)); + var enumerator = directory.enumerate_children ( + "standard::*", + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + + FileInfo info = null; + while ((info = enumerator.next_file (null)) != null) { + var file = directory.resolve_relative_path (info.get_name ()); + if (info.get_file_type () == FileType.DIRECTORY) { + continue; + } else { + if (file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null)) { + var audio_object = new AudioObject (file.get_uri ()); + + string? basename = file.get_basename (); + + if (basename != null) { + audio_object.title = basename; + } else { + audio_object.title = audio_object.uri; + } + + songs.append (audio_object); + } + } + } + } +} diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index 1d77624e7..a5d3435aa 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -1,6 +1,7 @@ public class Music.LibraryView : Gtk.Box { construct { var library_manager = LibraryManager.get_instance (); + var playback_manager = PlaybackManager.get_default (); var selection_model = new Gtk.SingleSelection (library_manager.songs); @@ -13,6 +14,11 @@ public class Music.LibraryView : Gtk.Box { }; append (list_view); + + selection_model.selection_changed.connect (() => { + var index = selection_model.get_selected (); + playback_manager.current_audio = (AudioObject)library_manager.songs.get_item (index); + }); } private void setup_widget (Object obj) { From 0bc75b8485e18607764c9a6cc69a52c1aa054980 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 15:35:39 +0200 Subject: [PATCH 04/27] Use a stackswitcher --- src/MainWindow.vala | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index a8cd9ee08..a98b99664 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -4,36 +4,33 @@ */ public class Music.MainWindow : Gtk.ApplicationWindow { - private Gtk.Button back_button; - private QueueView queue_view; private Gtk.Stack stack; private Settings settings; construct { var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); - back_button = new Gtk.Button.with_label (_("Library")); - back_button.add_css_class (Granite.STYLE_CLASS_BACK_BUTTON); - - var queue_button = new Gtk.Button.from_icon_name ("view-list-symbolic"); + var stack_switcher = new Gtk.StackSwitcher () { + hexpand = false + }; + ((Gtk.BoxLayout)stack_switcher.get_layout_manager ()).homogeneous = true; var start_header = new Gtk.HeaderBar () { show_title_buttons = false, - title_widget = new Gtk.Label ("") + title_widget = stack_switcher }; start_header.add_css_class (Granite.STYLE_CLASS_FLAT); start_header.pack_start (start_window_controls); - start_header.pack_start (back_button); - start_header.pack_end (queue_button); - queue_view = new QueueView (); + var queue_view = new QueueView (); var library_view = new LibraryView (); stack = new Gtk.Stack (); - stack.add_child (queue_view); - stack.add_child (library_view); - stack.visible_child = library_view; + stack.add_titled (library_view, null, _("Library")); + stack.add_titled (queue_view, null, _("Play Queue")); + + stack_switcher.stack = stack; var start_box = new Gtk.Box (VERTICAL, 0); start_box.add_css_class (Granite.STYLE_CLASS_VIEW); @@ -83,20 +80,5 @@ public class Music.MainWindow : Gtk.ApplicationWindow { settings = new Settings ("io.elementary.music"); settings.bind ("pane-position", paned, "position", SettingsBindFlags.DEFAULT); - - update_header_buttons (); - stack.notify["visible-child"].connect (update_header_buttons); - - queue_button.clicked.connect (() => { - stack.visible_child = queue_view; - }); - - back_button.clicked.connect (() => { - stack.visible_child = library_view; - }); - } - - private void update_header_buttons () { - back_button.visible = stack.visible_child == queue_view; } } From a51ea8163e88d256eaea593fc4b5d70f6ca1bb3f Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 16:30:13 +0200 Subject: [PATCH 05/27] Make previous/next work --- src/MainWindow.vala | 48 ++++++++++++++++++++++- src/PlaybackManager.vala | 80 +++++++++++++++++++------------------- src/Views/LibraryView.vala | 33 ++++++++++++++-- src/Views/QueueView.vala | 52 ------------------------- 4 files changed, 115 insertions(+), 98 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index a98b99664..42391d1f7 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -4,7 +4,8 @@ */ public class Music.MainWindow : Gtk.ApplicationWindow { - private Gtk.Stack stack; + private Gtk.Button repeat_button; + private Gtk.Button shuffle_button; private Settings settings; construct { @@ -15,18 +16,32 @@ public class Music.MainWindow : Gtk.ApplicationWindow { }; ((Gtk.BoxLayout)stack_switcher.get_layout_manager ()).homogeneous = true; + shuffle_button = new Gtk.Button.from_icon_name ("media-playlist-shuffle-symbolic") { + action_name = Application.ACTION_PREFIX + Application.ACTION_SHUFFLE, + tooltip_text = _("Shuffle") + }; + + repeat_button = new Gtk.Button (); + + var queue_header = new Gtk.HeaderBar () { + show_title_buttons = false, + title_widget = new Gtk.Label ("") + }; + var start_header = new Gtk.HeaderBar () { show_title_buttons = false, title_widget = stack_switcher }; start_header.add_css_class (Granite.STYLE_CLASS_FLAT); start_header.pack_start (start_window_controls); + start_header.pack_end (shuffle_button); + start_header.pack_end (repeat_button); var queue_view = new QueueView (); var library_view = new LibraryView (); - stack = new Gtk.Stack (); + var stack = new Gtk.Stack (); stack.add_titled (library_view, null, _("Library")); stack.add_titled (queue_view, null, _("Play Queue")); @@ -80,5 +95,34 @@ public class Music.MainWindow : Gtk.ApplicationWindow { settings = new Settings ("io.elementary.music"); settings.bind ("pane-position", paned, "position", SettingsBindFlags.DEFAULT); + settings.changed["repeat-mode"].connect (update_repeat_button); + + update_repeat_button (); + + repeat_button.clicked.connect (() => { + var enum_step = settings.get_enum ("repeat-mode"); + if (enum_step < 2) { + settings.set_enum ("repeat-mode", enum_step + 1); + } else { + settings.set_enum ("repeat-mode", 0); + } + }); + } + + private void update_repeat_button () { + switch (settings.get_string ("repeat-mode")) { + case "disabled": + repeat_button.icon_name = "media-playlist-no-repeat-symbolic"; + repeat_button.tooltip_text = _("Repeat None"); + break; + case "all": + repeat_button.icon_name = "media-playlist-repeat-symbolic"; + repeat_button.tooltip_text = _("Repeat All"); + break; + case "one": + repeat_button.icon_name = "media-playlist-repeat-song-symbolic"; + repeat_button.tooltip_text = _("Repeat One"); + break; + } } } diff --git a/src/PlaybackManager.vala b/src/PlaybackManager.vala index abdc6bdfc..8f3a00368 100644 --- a/src/PlaybackManager.vala +++ b/src/PlaybackManager.vala @@ -4,10 +4,13 @@ */ public class Music.PlaybackManager : Object { + public signal bool ask_has_previous (); + public signal bool ask_has_next (bool repeat_all); + public signal void invalids_found (int count); + public AudioObject? current_audio { get; set; default = null; } public ListStore queue_liststore { get; private set; } public int64 playback_position { get; private set; } - public signal void invalids_found (int count); private static GLib.Once instance; public static unowned PlaybackManager get_default () { @@ -263,43 +266,19 @@ public class Music.PlaybackManager : Object { public void next (bool eos = false) { direction = Direction.NEXT; next_by_eos = eos; - uint position = -1; - queue_liststore.find (current_audio, out position); - - if (position != -1) { - if (!next_by_eos) { - if (position == queue_liststore.get_n_items () - 1) { - current_audio = (AudioObject) queue_liststore.get_item (0); - if (position == 0) { - seek_to_progress (0); - } - } else { - current_audio = (AudioObject) queue_liststore.get_item (position + 1); - } + uint position; + bool from_queue = queue_liststore.find (current_audio, out position); - return; - } + if (next_by_eos) { switch (settings.get_string ("repeat-mode")) { case "disabled": - if (position == queue_liststore.get_n_items () - 1) { - current_audio = null; - return; - } - - current_audio = (AudioObject) queue_liststore.get_item (position + 1); - break; case "all": - if (position == queue_liststore.get_n_items () - 1) { - current_audio = (AudioObject) queue_liststore.get_item (0); - if (position == 0) { - seek_to_progress (0); - } - } else { - current_audio = (AudioObject) queue_liststore.get_item (position + 1); + if (!from_queue) { + ask_has_next (true); + return; } - break; case "one": @@ -307,25 +286,44 @@ public class Music.PlaybackManager : Object { break; } } + + if (from_queue) { + if (position == queue_liststore.get_n_items () - 1) { + current_audio = (AudioObject) queue_liststore.get_item (0); + if (position == 0) { + seek_to_progress (0); + } + } else { + current_audio = (AudioObject) queue_liststore.get_item (position + 1); + } + + return; + } + + ask_has_next (false); } public void previous () { direction = Direction.PREVIOUS; uint position = -1; - queue_liststore.find (current_audio, out position); - if (position != -1 && position != 0) { - current_audio = (AudioObject) queue_liststore.get_item (position - 1); - } + if (queue_liststore.find (current_audio, out position)) { + if (position == 0) { + uint n_items = queue_liststore.get_n_items (); + if (n_items == 1) { + seek_to_progress (0); + } else { + current_audio = (AudioObject) queue_liststore.get_item (n_items - 1); + } - if (position == 0) { - uint n_items = queue_liststore.get_n_items (); - if (n_items == 1) { - seek_to_progress (0); - } else { - current_audio = (AudioObject) queue_liststore.get_item (n_items - 1); + return; } + + current_audio = (AudioObject) queue_liststore.get_item (position - 1); + return; } + + ask_has_previous (); } public void shuffle () { diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index a5d3435aa..5fbdd5564 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -3,7 +3,10 @@ public class Music.LibraryView : Gtk.Box { var library_manager = LibraryManager.get_instance (); var playback_manager = PlaybackManager.get_default (); - var selection_model = new Gtk.SingleSelection (library_manager.songs); + var selection_model = new Gtk.SingleSelection (library_manager.songs) { + can_unselect = true, + autoselect = false + }; var factory = new Gtk.SignalListItemFactory (); factory.setup.connect (setup_widget); @@ -15,10 +18,34 @@ public class Music.LibraryView : Gtk.Box { append (list_view); + playback_manager.ask_has_next.connect ((repeat_all) => { + if (selection_model.selected < selection_model.get_n_items () - 1) { + selection_model.set_selected (selection_model.selected + 1); + return true; + } else if (repeat_all) { + selection_model.set_selected (0); + return true; + } + + return false; + }); + + playback_manager.ask_has_previous.connect (() => { + if (selection_model.selected > 0) { + selection_model.set_selected (selection_model.selected - 1); + } else { + selection_model.set_selected (selection_model.get_n_items () - 1); + } + + return true; + }); + selection_model.selection_changed.connect (() => { - var index = selection_model.get_selected (); - playback_manager.current_audio = (AudioObject)library_manager.songs.get_item (index); + //TODO: Should clear play queue? + playback_manager.current_audio = (AudioObject)selection_model.get_selected_item (); }); + + selection_model.set_selected (Gtk.INVALID_LIST_POSITION); } private void setup_widget (Object obj) { diff --git a/src/Views/QueueView.vala b/src/Views/QueueView.vala index 14a8aee0f..77f24602a 100644 --- a/src/Views/QueueView.vala +++ b/src/Views/QueueView.vala @@ -1,30 +1,9 @@ public class Music.QueueView : Gtk.Box { - private Gtk.Button repeat_button; - private Gtk.Button shuffle_button; - private Settings settings; - construct { var playback_manager = PlaybackManager.get_default (); var start_window_controls = new Gtk.WindowControls (Gtk.PackType.START); - shuffle_button = new Gtk.Button.from_icon_name ("media-playlist-shuffle-symbolic") { - action_name = Application.ACTION_PREFIX + Application.ACTION_SHUFFLE, - tooltip_text = _("Shuffle") - }; - - repeat_button = new Gtk.Button (); - - var queue_header = new Gtk.HeaderBar () { - show_title_buttons = false, - title_widget = new Gtk.Label ("") - }; - queue_header.add_css_class (Granite.STYLE_CLASS_FLAT); - queue_header.add_css_class (Granite.STYLE_CLASS_DEFAULT_DECORATION); - queue_header.pack_start (start_window_controls); - queue_header.pack_end (shuffle_button); - queue_header.pack_end (repeat_button); - var queue_placeholder = new Granite.Placeholder (_("Queue is Empty")) { description = _("Audio files opened from Files will appear here"), icon = new ThemedIcon ("playlist-queue") @@ -64,11 +43,6 @@ public class Music.QueueView : Gtk.Box { vexpand = true; append (queue_handle); - settings = new Settings ("io.elementary.music"); - settings.changed["repeat-mode"].connect (update_repeat_button); - - update_repeat_button (); - drop_target.drop.connect ((target, value, x, y) => { if (value.type () == typeof (Gdk.FileList)) { File[] files; @@ -103,15 +77,6 @@ public class Music.QueueView : Gtk.Box { error_toast.send_notification (); }); - repeat_button.clicked.connect (() => { - var enum_step = settings.get_enum ("repeat-mode"); - if (enum_step < 2) { - settings.set_enum ("repeat-mode", enum_step + 1); - } else { - settings.set_enum ("repeat-mode", 0); - } - }); - queue_listbox.row_activated.connect ((row) => { playback_manager.current_audio = ((TrackRow) row).audio_object; }); @@ -140,23 +105,6 @@ public class Music.QueueView : Gtk.Box { } } - private void update_repeat_button () { - switch (settings.get_string ("repeat-mode")) { - case "disabled": - repeat_button.icon_name = "media-playlist-no-repeat-symbolic"; - repeat_button.tooltip_text = _("Repeat None"); - break; - case "all": - repeat_button.icon_name = "media-playlist-repeat-symbolic"; - repeat_button.tooltip_text = _("Repeat All"); - break; - case "one": - repeat_button.icon_name = "media-playlist-repeat-song-symbolic"; - repeat_button.tooltip_text = _("Repeat One"); - break; - } - } - private Gtk.Widget create_queue_row (GLib.Object object) { unowned var audio_object = (AudioObject) object; var track_row = new TrackRow (); From e7267eaa0000b9570bebf8631da8487209a17189 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 16:40:15 +0200 Subject: [PATCH 06/27] Cleanup --- src/{ => Services}/PlaybackManager.vala | 27 +++++++++++-------------- src/Views/LibraryView.vala | 6 +++++- src/meson.build | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) rename src/{ => Services}/PlaybackManager.vala (96%) diff --git a/src/PlaybackManager.vala b/src/Services/PlaybackManager.vala similarity index 96% rename from src/PlaybackManager.vala rename to src/Services/PlaybackManager.vala index 8f3a00368..68ce85ae8 100644 --- a/src/PlaybackManager.vala +++ b/src/Services/PlaybackManager.vala @@ -269,6 +269,11 @@ public class Music.PlaybackManager : Object { uint position; bool from_queue = queue_liststore.find (current_audio, out position); + if (from_queue && position != queue_liststore.get_n_items () - 1) { + current_audio = (AudioObject) queue_liststore.get_item (position + 1); + return; + } + if (next_by_eos) { switch (settings.get_string ("repeat-mode")) { case "disabled": @@ -278,26 +283,18 @@ public class Music.PlaybackManager : Object { if (!from_queue) { ask_has_next (true); return; + } else { + current_audio = (AudioObject) queue_liststore.get_item (0); + if (position == 0) { + seek_to_progress (0); + } + return; } - break; case "one": seek_to_progress (0); - break; - } - } - - if (from_queue) { - if (position == queue_liststore.get_n_items () - 1) { - current_audio = (AudioObject) queue_liststore.get_item (0); - if (position == 0) { - seek_to_progress (0); - } - } else { - current_audio = (AudioObject) queue_liststore.get_item (position + 1); + return; } - - return; } ask_has_next (false); diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index 5fbdd5564..cfb1d860f 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -16,7 +16,11 @@ public class Music.LibraryView : Gtk.Box { hexpand = true }; - append (list_view); + var scrolled_window = new Gtk.ScrolledWindow () { + child = list_view + }; + + append (scrolled_window); playback_manager.ask_has_next.connect ((repeat_all) => { if (selection_model.selected < selection_model.get_n_items () - 1) { diff --git a/src/meson.build b/src/meson.build index a8cee7830..44e3a9be4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,10 +2,10 @@ sources = [ 'Application.vala', 'AudioObject.vala', 'MainWindow.vala', - 'PlaybackManager.vala', 'DBus/MprisPlayer.vala', 'DBus/MprisRoot.vala', 'Services/LibraryManager.vala', + 'Services/PlaybackManager.vala', 'Views/LibraryView.vala', 'Views/NowPlayingView.vala', 'Views/QueueView.vala', From 31172acdd716ccd323c17a95aa9262e8069453e2 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 16:51:34 +0200 Subject: [PATCH 07/27] Add placeholder --- src/Views/LibraryView.vala | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index cfb1d860f..88421d612 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -1,9 +1,17 @@ public class Music.LibraryView : Gtk.Box { + private Gtk.Stack placeholder_stack; + private Gtk.SingleSelection selection_model; + construct { var library_manager = LibraryManager.get_instance (); var playback_manager = PlaybackManager.get_default (); - var selection_model = new Gtk.SingleSelection (library_manager.songs) { + var placeholder = new Granite.Placeholder (_("No Songs found")) { + description = _("Audio files in your Music directory will appear here"), + icon = new ThemedIcon ("folder-music") + }; + + selection_model = new Gtk.SingleSelection (library_manager.songs) { can_unselect = true, autoselect = false }; @@ -20,9 +28,27 @@ public class Music.LibraryView : Gtk.Box { child = list_view }; - append (scrolled_window); + placeholder_stack = new Gtk.Stack (); + placeholder_stack.add_named (scrolled_window, "list-view"); + placeholder_stack.add_named (placeholder, "placeholder"); + + append (placeholder_stack); + + selection_model.items_changed.connect (update_stack); + + selection_model.selection_changed.connect (() => { + //TODO: Should clear play queue? + playback_manager.current_audio = (AudioObject)selection_model.get_selected_item (); + }); + + selection_model.set_selected (Gtk.INVALID_LIST_POSITION); + update_stack (); playback_manager.ask_has_next.connect ((repeat_all) => { + if (selection_model.get_n_items () == 0) { + return false; + } + if (selection_model.selected < selection_model.get_n_items () - 1) { selection_model.set_selected (selection_model.selected + 1); return true; @@ -35,6 +61,10 @@ public class Music.LibraryView : Gtk.Box { }); playback_manager.ask_has_previous.connect (() => { + if (selection_model.get_n_items () == 0) { + return false; + } + if (selection_model.selected > 0) { selection_model.set_selected (selection_model.selected - 1); } else { @@ -43,13 +73,10 @@ public class Music.LibraryView : Gtk.Box { return true; }); + } - selection_model.selection_changed.connect (() => { - //TODO: Should clear play queue? - playback_manager.current_audio = (AudioObject)selection_model.get_selected_item (); - }); - - selection_model.set_selected (Gtk.INVALID_LIST_POSITION); + private void update_stack () { + placeholder_stack.visible_child_name = selection_model.get_n_items () > 0 ? "list-view" : "placeholder"; } private void setup_widget (Object obj) { From b11ddac0deaace5d9e735e2ba0a9c6371dc4384e Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 17:45:58 +0200 Subject: [PATCH 08/27] Implement recursive directory searching and directory monitoring --- src/Services/LibraryManager.vala | 131 +++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 24 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 4ff3c1273..a2380da3f 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -1,6 +1,11 @@ public class Music.LibraryManager : Object { public ListStore songs { get; construct; } + private List directory_monitors; + private Queue unchecked_directories; + private HashTable position_by_uri; + private bool is_scanning = false; + private static GLib.Once instance; public static unowned LibraryManager get_instance () { return instance.once (() => { return new LibraryManager (); }); @@ -8,37 +13,115 @@ public class Music.LibraryManager : Object { construct { songs = new ListStore (typeof (AudioObject)); + directory_monitors = new List (); + unchecked_directories = new Queue (); + unchecked_directories.push_tail (File.new_for_path (Environment.get_user_special_dir (UserDirectory.MUSIC))); + position_by_uri = new HashTable (str_hash, str_equal); + detect_audio_files.begin (); } - public async void detect_audio_files () throws Error { - File directory = File.new_for_path (Environment.get_user_special_dir (UserDirectory.MUSIC)); - var enumerator = directory.enumerate_children ( - "standard::*", - FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - null - ); - - FileInfo info = null; - while ((info = enumerator.next_file (null)) != null) { - var file = directory.resolve_relative_path (info.get_name ()); - if (info.get_file_type () == FileType.DIRECTORY) { - continue; - } else { - if (file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null)) { - var audio_object = new AudioObject (file.get_uri ()); - - string? basename = file.get_basename (); - - if (basename != null) { - audio_object.title = basename; + private void on_directory_change (File file, File? other_file, FileMonitorEvent event_type) { + uint position = Gtk.INVALID_LIST_POSITION; + if (file.get_uri () in position_by_uri) { + position = position_by_uri[file.get_uri ()]; + } + + switch (event_type) { + case DELETED: + if (position != Gtk.INVALID_LIST_POSITION) { + songs.remove (position); + } + break; + + case CHANGES_DONE_HINT: + if (position != Gtk.INVALID_LIST_POSITION) { + songs.remove (position); + } + + FileInfo file_info; + try { + file_info = file.query_info ("standard::*", NONE); + } catch (Error e) { + warning (e.message); + return; + } + + if (file_info.get_file_type () == FileType.DIRECTORY) { + unchecked_directories.push_tail (file); + detect_audio_files.begin (); + } else if (is_file_valid (file)) { + songs.append (create_audio_object (file)); + } + + break; + + default: + break; + } + } + + public async void detect_audio_files () { + if (is_scanning) { + return; + } + + is_scanning = true; + + while (!unchecked_directories.is_empty ()) { + var directory = unchecked_directories.pop_head (); + + try { + var directory_monitor = directory.monitor (NONE); + directory_monitor.changed.connect (on_directory_change); + directory_monitors.append (directory_monitor); + } catch (Error e) { + warning ("Failed to monitor directory %s: %s", directory.get_path (), e.message); + } + + try { + var enumerator = directory.enumerate_children ( + "standard::*", + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + + FileInfo info = null; + while ((info = enumerator.next_file (null)) != null) { + var file = directory.resolve_relative_path (info.get_name ()); + if (info.get_file_type () == FileType.DIRECTORY) { + unchecked_directories.push_tail (file); + continue; } else { - audio_object.title = audio_object.uri; + if (is_file_valid (file)) { + songs.append (create_audio_object (file)); + position_by_uri[file.get_uri ()] = songs.get_n_items () - 1; + } } - - songs.append (audio_object); } + } catch (Error e) { + warning ("Failed to get children of directory %s: %s", directory.get_path (), e.message); } } + + is_scanning = false; + } + + private bool is_file_valid (File file) { + return file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null); + } + + private AudioObject create_audio_object (File file) { + var audio_object = new AudioObject (file.get_uri ()); + + string? basename = file.get_basename (); + + if (basename != null) { + audio_object.title = basename; + } else { + audio_object.title = audio_object.uri; + } + + return audio_object; } } From ce4870764dfd8d363717f088b157bbe13761812c Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 18:14:54 +0200 Subject: [PATCH 09/27] Cleanup --- src/Views/QueueView.vala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Views/QueueView.vala b/src/Views/QueueView.vala index 77f24602a..92a7866b0 100644 --- a/src/Views/QueueView.vala +++ b/src/Views/QueueView.vala @@ -21,16 +21,12 @@ public class Music.QueueView : Gtk.Box { }; var drop_target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); - - var queue = new Gtk.Grid (); - // queue.attach (queue_header, 0, 0); - queue.attach (scrolled, 0, 1); - queue.add_controller (drop_target); + scrolled.add_controller (drop_target); var error_toast = new Granite.Toast (""); var queue_overlay = new Gtk.Overlay () { - child = queue + child = scrolled }; queue_overlay.add_overlay (error_toast); From a59561c659b7b4df1ba1b86bc1bd988cc8790b18 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 18:19:17 +0200 Subject: [PATCH 10/27] Cleanup --- src/AudioObject.vala | 12 ++++++++++++ src/Services/LibraryManager.vala | 18 ++---------------- src/Services/PlaybackManager.vala | 14 ++------------ 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index b80934a9d..637802c62 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -13,4 +13,16 @@ public class Music.AudioObject : Object { public AudioObject (string uri) { Object (uri: uri); } + + public AudioObject.from_file (File file) { + base (file.get_uri ()); + + string? basename = file.get_basename (); + + if (basename != null) { + title = basename; + } else { + title = uri; + } + } } diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index a2380da3f..f64ac4135 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -51,7 +51,7 @@ public class Music.LibraryManager : Object { unchecked_directories.push_tail (file); detect_audio_files.begin (); } else if (is_file_valid (file)) { - songs.append (create_audio_object (file)); + songs.append (AudioObject.from_file (file)); } break; @@ -94,7 +94,7 @@ public class Music.LibraryManager : Object { continue; } else { if (is_file_valid (file)) { - songs.append (create_audio_object (file)); + songs.append (new AudioObject.from_file (file)); position_by_uri[file.get_uri ()] = songs.get_n_items () - 1; } } @@ -110,18 +110,4 @@ public class Music.LibraryManager : Object { private bool is_file_valid (File file) { return file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null); } - - private AudioObject create_audio_object (File file) { - var audio_object = new AudioObject (file.get_uri ()); - - string? basename = file.get_basename (); - - if (basename != null) { - audio_object.title = basename; - } else { - audio_object.title = audio_object.uri; - } - - return audio_object; - } } diff --git a/src/Services/PlaybackManager.vala b/src/Services/PlaybackManager.vala index 68ce85ae8..1e80ae15c 100644 --- a/src/Services/PlaybackManager.vala +++ b/src/Services/PlaybackManager.vala @@ -92,19 +92,9 @@ public class Music.PlaybackManager : Object { int invalids = 0; foreach (unowned var file in files) { if (file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null)) { - var audio_object = new AudioObject (file.get_uri ()); - - string? basename = file.get_basename (); - - if (basename != null) { - audio_object.title = basename; - } else { - audio_object.title = audio_object.uri; - } - - discoverer.discover_uri_async (audio_object.uri); - + var audio_object = new AudioObject.from_file (file); queue_liststore.append (audio_object); + discoverer.discover_uri_async (audio_object.uri); } else { invalids++; continue; From 172f05190a85024dba9c8b208fedaf5a345a1627 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 23 Jul 2023 21:04:03 +0200 Subject: [PATCH 11/27] Fix --- src/Services/LibraryManager.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index f64ac4135..a3675ce5b 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -51,7 +51,7 @@ public class Music.LibraryManager : Object { unchecked_directories.push_tail (file); detect_audio_files.begin (); } else if (is_file_valid (file)) { - songs.append (AudioObject.from_file (file)); + songs.append (new AudioObject.from_file (file)); } break; From ee4fe2d58478d01560328e5cd5b82e809e97cb3c Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 16:14:44 +0200 Subject: [PATCH 12/27] We've succesfully queried something --- io.elementary.music.yml | 3 +- meson.build | 4 +- src/Services/LibraryManager.vala | 204 +++++++++++++++++-------------- 3 files changed, 119 insertions(+), 92 deletions(-) diff --git a/io.elementary.music.yml b/io.elementary.music.yml index b2b787393..d1fb49f52 100644 --- a/io.elementary.music.yml +++ b/io.elementary.music.yml @@ -9,7 +9,8 @@ finish-args: - '--socket=wayland' - '--socket=pulseaudio' - '--device=dri' - - '--filesystem=xdg-music:ro' + - '--filesystem=xdg-music' + - '--add-policy=Tracker3.dbus:org.freedesktop.Tracker3.Miner.Files=*' modules: - name: music diff --git a/meson.build b/meson.build index 1cd30dea7..470f7aff5 100644 --- a/meson.build +++ b/meson.build @@ -22,13 +22,15 @@ gstreamer_dep = dependency('gstreamer-1.0') gstreamer_pbutils_dep = dependency('gstreamer-pbutils-1.0') gstreamer_tag_dep = dependency('gstreamer-tag-1.0') gtk_dep = dependency('gtk4') +tracker_dep = dependency('tracker-sparql-3.0') dependencies = [ granite_dep, gstreamer_dep, gstreamer_pbutils_dep, gstreamer_tag_dep, - gtk_dep + gtk_dep, + tracker_dep ] meson.add_install_script('meson/post_install.py') diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index a3675ce5b..f92c53f19 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -1,10 +1,10 @@ public class Music.LibraryManager : Object { public ListStore songs { get; construct; } - private List directory_monitors; - private Queue unchecked_directories; - private HashTable position_by_uri; - private bool is_scanning = false; + // private List directory_monitors; + // private Queue unchecked_directories; + // private HashTable position_by_uri; + // private bool is_scanning = false; private static GLib.Once instance; public static unowned LibraryManager get_instance () { @@ -13,101 +13,125 @@ public class Music.LibraryManager : Object { construct { songs = new ListStore (typeof (AudioObject)); - directory_monitors = new List (); - unchecked_directories = new Queue (); - unchecked_directories.push_tail (File.new_for_path (Environment.get_user_special_dir (UserDirectory.MUSIC))); - position_by_uri = new HashTable (str_hash, str_equal); - detect_audio_files.begin (); - } + try { + var tracker_connection = Tracker.Sparql.Connection.bus_new ("org.freedesktop.Tracker3.Miner.Files", null, null); - private void on_directory_change (File file, File? other_file, FileMonitorEvent event_type) { - uint position = Gtk.INVALID_LIST_POSITION; - if (file.get_uri () in position_by_uri) { - position = position_by_uri[file.get_uri ()]; - } + var tracker_statement = tracker_connection.query_statement ( + """SELECT nie:url(?u) WHERE { ?u a nfo:FileDataObject ; nfo:fileName "dicht & ergreifend - Zipfeschwinga.mp3" }""" + ); - switch (event_type) { - case DELETED: - if (position != Gtk.INVALID_LIST_POSITION) { - songs.remove (position); - } - break; - - case CHANGES_DONE_HINT: - if (position != Gtk.INVALID_LIST_POSITION) { - songs.remove (position); - } - - FileInfo file_info; - try { - file_info = file.query_info ("standard::*", NONE); - } catch (Error e) { - warning (e.message); - return; - } - - if (file_info.get_file_type () == FileType.DIRECTORY) { - unchecked_directories.push_tail (file); - detect_audio_files.begin (); - } else if (is_file_valid (file)) { - songs.append (new AudioObject.from_file (file)); - } - - break; - - default: - break; - } - } + print ("execute"); + var cursor = tracker_statement.execute (null); - public async void detect_audio_files () { - if (is_scanning) { - return; - } + print ("finish execute"); - is_scanning = true; - - while (!unchecked_directories.is_empty ()) { - var directory = unchecked_directories.pop_head (); - - try { - var directory_monitor = directory.monitor (NONE); - directory_monitor.changed.connect (on_directory_change); - directory_monitors.append (directory_monitor); - } catch (Error e) { - warning ("Failed to monitor directory %s: %s", directory.get_path (), e.message); + int i = 0; + while (cursor.next ()) { + print ("Result [%i]: %s\n", i++, cursor.get_string (0)); } + print ("finished"); - try { - var enumerator = directory.enumerate_children ( - "standard::*", - FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - null - ); - - FileInfo info = null; - while ((info = enumerator.next_file (null)) != null) { - var file = directory.resolve_relative_path (info.get_name ()); - if (info.get_file_type () == FileType.DIRECTORY) { - unchecked_directories.push_tail (file); - continue; - } else { - if (is_file_valid (file)) { - songs.append (new AudioObject.from_file (file)); - position_by_uri[file.get_uri ()] = songs.get_n_items () - 1; - } - } - } - } catch (Error e) { - warning ("Failed to get children of directory %s: %s", directory.get_path (), e.message); - } + cursor.close (); + tracker_connection.close (); + } catch (Error e) { + warning (e.message); } + // directory_monitors = new List (); + // unchecked_directories = new Queue (); + // unchecked_directories.push_tail (File.new_for_path (Environment.get_user_special_dir (UserDirectory.MUSIC))); + // position_by_uri = new HashTable (str_hash, str_equal); - is_scanning = false; + // detect_audio_files.begin (); } - private bool is_file_valid (File file) { - return file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null); - } + // private void on_directory_change (File file, File? other_file, FileMonitorEvent event_type) { + // uint position = Gtk.INVALID_LIST_POSITION; + // if (file.get_uri () in position_by_uri) { + // position = position_by_uri[file.get_uri ()]; + // } + + // switch (event_type) { + // case DELETED: + // if (position != Gtk.INVALID_LIST_POSITION) { + // songs.remove (position); + // } + // break; + + // case CHANGES_DONE_HINT: + // if (position != Gtk.INVALID_LIST_POSITION) { + // songs.remove (position); + // } + + // FileInfo file_info; + // try { + // file_info = file.query_info ("standard::*", NONE); + // } catch (Error e) { + // warning (e.message); + // return; + // } + + // if (file_info.get_file_type () == FileType.DIRECTORY) { + // unchecked_directories.push_tail (file); + // detect_audio_files.begin (); + // } else if (is_file_valid (file)) { + // songs.append (new AudioObject.from_file (file)); + // } + + // break; + + // default: + // break; + // } + // } + + // public async void detect_audio_files () { + // if (is_scanning) { + // return; + // } + + // is_scanning = true; + + // while (!unchecked_directories.is_empty ()) { + // var directory = unchecked_directories.pop_head (); + + // try { + // var directory_monitor = directory.monitor (NONE); + // directory_monitor.changed.connect (on_directory_change); + // directory_monitors.append (directory_monitor); + // } catch (Error e) { + // warning ("Failed to monitor directory %s: %s", directory.get_path (), e.message); + // } + + // try { + // var enumerator = directory.enumerate_children ( + // "standard::*", + // FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + // null + // ); + + // FileInfo info = null; + // while ((info = enumerator.next_file (null)) != null) { + // var file = directory.resolve_relative_path (info.get_name ()); + // if (info.get_file_type () == FileType.DIRECTORY) { + // unchecked_directories.push_tail (file); + // continue; + // } else { + // if (is_file_valid (file)) { + // songs.append (new AudioObject.from_file (file)); + // position_by_uri[file.get_uri ()] = songs.get_n_items () - 1; + // } + // } + // } + // } catch (Error e) { + // warning ("Failed to get children of directory %s: %s", directory.get_path (), e.message); + // } + // } + + // is_scanning = false; + // } + + // private bool is_file_valid (File file) { + // return file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null); + // } } From 6e874f30e4a2b0c97017cbf7110e9e9b969a3e3c Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 16:36:29 +0200 Subject: [PATCH 13/27] Detect audio files with artist title and duration --- src/Services/LibraryManager.vala | 63 ++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index f92c53f19..d04473ba7 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -1,10 +1,7 @@ public class Music.LibraryManager : Object { public ListStore songs { get; construct; } - // private List directory_monitors; - // private Queue unchecked_directories; - // private HashTable position_by_uri; - // private bool is_scanning = false; + private Tracker.Sparql.Connection tracker_connection; private static GLib.Once instance; public static unowned LibraryManager get_instance () { @@ -15,34 +12,62 @@ public class Music.LibraryManager : Object { songs = new ListStore (typeof (AudioObject)); try { - var tracker_connection = Tracker.Sparql.Connection.bus_new ("org.freedesktop.Tracker3.Miner.Files", null, null); + tracker_connection = Tracker.Sparql.Connection.bus_new ("org.freedesktop.Tracker3.Miner.Files", null, null); + get_audio_files.begin (); + } catch (Error e) { + warning (e.message); + } + } + + private async void get_audio_files () { + try { var tracker_statement = tracker_connection.query_statement ( - """SELECT nie:url(?u) WHERE { ?u a nfo:FileDataObject ; nfo:fileName "dicht & ergreifend - Zipfeschwinga.mp3" }""" + """ + SELECT ?url ?title ?artist ?duration + WHERE { + ?song a nmm:MusicPiece ; + nie:isStoredAs ?as . + ?as nie:url ?url . + OPTIONAL { + ?song nie:title ?title + } . + OPTIONAL { + ?song nmm:artist [ nmm:artistName ?artist ] ; + } . + OPTIONAL { + ?song nfo:duration ?duration ; + } . + } + """ ); - print ("execute"); var cursor = tracker_statement.execute (null); - print ("finish execute"); - - int i = 0; while (cursor.next ()) { - print ("Result [%i]: %s\n", i++, cursor.get_string (0)); + var audio_object = new AudioObject (cursor.get_string (0)); + + if (cursor.is_bound (1)) { + audio_object.title = cursor.get_string (1); + } else { + audio_object.title = audio_object.uri; + } + + if (cursor.is_bound (2)) { + audio_object.artist = cursor.get_string (2); + } + + if (cursor.is_bound (3)) { + audio_object.duration = cursor.get_integer (3); + } + + songs.append (audio_object); } - print ("finished"); cursor.close (); - tracker_connection.close (); } catch (Error e) { warning (e.message); } - // directory_monitors = new List (); - // unchecked_directories = new Queue (); - // unchecked_directories.push_tail (File.new_for_path (Environment.get_user_special_dir (UserDirectory.MUSIC))); - // position_by_uri = new HashTable (str_hash, str_equal); - - // detect_audio_files.begin (); } // private void on_directory_change (File file, File? other_file, FileMonitorEvent event_type) { From b1749f7f7ef2eacea071036de3a8a18f0a3bb7aa Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 17:23:51 +0200 Subject: [PATCH 14/27] Add notifier (not working yet) --- src/Services/LibraryManager.vala | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index d04473ba7..5fc98e360 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -14,6 +14,11 @@ public class Music.LibraryManager : Object { try { tracker_connection = Tracker.Sparql.Connection.bus_new ("org.freedesktop.Tracker3.Miner.Files", null, null); + var notifier = tracker_connection.create_notifier (); + if (notifier != null) { + notifier.events.connect (on_tracker_event); + } + get_audio_files.begin (); } catch (Error e) { warning (e.message); @@ -70,6 +75,27 @@ public class Music.LibraryManager : Object { } } + private void on_tracker_event (string service, string graph, GenericArray events) { + print (service); + print (graph); + foreach (var event in events) { + var type = event.get_event_type (); + switch (type) { + case DELETE: + print ("FILE DELETED"); + break; + + case CREATE: + print ("FILE CREATED"); + break; + + case UPDATE: + print ("FILE UPDATED"); + break; + } + } + } + // private void on_directory_change (File file, File? other_file, FileMonitorEvent event_type) { // uint position = Gtk.INVALID_LIST_POSITION; // if (file.get_uri () in position_by_uri) { From 92adb55daddc7593a5d1d72a035627496ae9174d Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 17:46:01 +0200 Subject: [PATCH 15/27] Use audio graph --- io.elementary.music.yml | 2 +- src/Services/LibraryManager.vala | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/io.elementary.music.yml b/io.elementary.music.yml index d1fb49f52..3d8ca8b3e 100644 --- a/io.elementary.music.yml +++ b/io.elementary.music.yml @@ -10,7 +10,7 @@ finish-args: - '--socket=pulseaudio' - '--device=dri' - '--filesystem=xdg-music' - - '--add-policy=Tracker3.dbus:org.freedesktop.Tracker3.Miner.Files=*' + - '--add-policy=Tracker3.dbus:org.freedesktop.Tracker3.Miner.Files=tracker:Audio' modules: - name: music diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 5fc98e360..4552ccf7e 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -31,18 +31,22 @@ public class Music.LibraryManager : Object { """ SELECT ?url ?title ?artist ?duration WHERE { - ?song a nmm:MusicPiece ; - nie:isStoredAs ?as . - ?as nie:url ?url . - OPTIONAL { - ?song nie:title ?title - } . - OPTIONAL { - ?song nmm:artist [ nmm:artistName ?artist ] ; - } . - OPTIONAL { - ?song nfo:duration ?duration ; - } . + GRAPH tracker:Audio { + SELECT ?url ?title ?artist ?duration + WHERE { + ?song a nmm:MusicPiece ; + nie:isStoredAs ?url . + OPTIONAL { + ?song nie:title ?title + } . + OPTIONAL { + ?song nmm:artist [ nmm:artistName ?artist ] ; + } . + OPTIONAL { + ?song nfo:duration ?duration ; + } . + } + } } """ ); From 9ff8e1f9c4595be68e9ddcdcf8dff28d9c566f67 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 17:47:02 +0200 Subject: [PATCH 16/27] Cleanup --- src/Services/LibraryManager.vala | 91 +------------------------------- 1 file changed, 1 insertion(+), 90 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 4552ccf7e..4c8548aa7 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -16,6 +16,7 @@ public class Music.LibraryManager : Object { var notifier = tracker_connection.create_notifier (); if (notifier != null) { + //TODO doesn't get emitted notifier.events.connect (on_tracker_event); } @@ -99,94 +100,4 @@ public class Music.LibraryManager : Object { } } } - - // private void on_directory_change (File file, File? other_file, FileMonitorEvent event_type) { - // uint position = Gtk.INVALID_LIST_POSITION; - // if (file.get_uri () in position_by_uri) { - // position = position_by_uri[file.get_uri ()]; - // } - - // switch (event_type) { - // case DELETED: - // if (position != Gtk.INVALID_LIST_POSITION) { - // songs.remove (position); - // } - // break; - - // case CHANGES_DONE_HINT: - // if (position != Gtk.INVALID_LIST_POSITION) { - // songs.remove (position); - // } - - // FileInfo file_info; - // try { - // file_info = file.query_info ("standard::*", NONE); - // } catch (Error e) { - // warning (e.message); - // return; - // } - - // if (file_info.get_file_type () == FileType.DIRECTORY) { - // unchecked_directories.push_tail (file); - // detect_audio_files.begin (); - // } else if (is_file_valid (file)) { - // songs.append (new AudioObject.from_file (file)); - // } - - // break; - - // default: - // break; - // } - // } - - // public async void detect_audio_files () { - // if (is_scanning) { - // return; - // } - - // is_scanning = true; - - // while (!unchecked_directories.is_empty ()) { - // var directory = unchecked_directories.pop_head (); - - // try { - // var directory_monitor = directory.monitor (NONE); - // directory_monitor.changed.connect (on_directory_change); - // directory_monitors.append (directory_monitor); - // } catch (Error e) { - // warning ("Failed to monitor directory %s: %s", directory.get_path (), e.message); - // } - - // try { - // var enumerator = directory.enumerate_children ( - // "standard::*", - // FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - // null - // ); - - // FileInfo info = null; - // while ((info = enumerator.next_file (null)) != null) { - // var file = directory.resolve_relative_path (info.get_name ()); - // if (info.get_file_type () == FileType.DIRECTORY) { - // unchecked_directories.push_tail (file); - // continue; - // } else { - // if (is_file_valid (file)) { - // songs.append (new AudioObject.from_file (file)); - // position_by_uri[file.get_uri ()] = songs.get_n_items () - 1; - // } - // } - // } - // } catch (Error e) { - // warning ("Failed to get children of directory %s: %s", directory.get_path (), e.message); - // } - // } - - // is_scanning = false; - // } - - // private bool is_file_valid (File file) { - // return file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null); - // } } From 687c06e798a1374fe869d19275116601a3b017e1 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 19:06:20 +0200 Subject: [PATCH 17/27] Notifying somewhat works now --- src/AudioObject.vala | 2 +- src/Services/LibraryManager.vala | 106 ++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index 637802c62..1d968897d 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -4,7 +4,7 @@ */ public class Music.AudioObject : Object { - public string uri { get; construct; } + public string uri { get; set; } public Gdk.Texture texture { get; set; } public string artist { get; set; } public string title { get; set; } diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 4c8548aa7..9fcc9b7d1 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -2,6 +2,8 @@ public class Music.LibraryManager : Object { public ListStore songs { get; construct; } private Tracker.Sparql.Connection tracker_connection; + private Tracker.Notifier notifier; + private HashTable songs_by_urn; private static GLib.Once instance; public static unowned LibraryManager get_instance () { @@ -10,13 +12,13 @@ public class Music.LibraryManager : Object { construct { songs = new ListStore (typeof (AudioObject)); + songs_by_urn = new HashTable (str_hash, str_equal); try { tracker_connection = Tracker.Sparql.Connection.bus_new ("org.freedesktop.Tracker3.Miner.Files", null, null); - var notifier = tracker_connection.create_notifier (); + notifier = tracker_connection.create_notifier (); if (notifier != null) { - //TODO doesn't get emitted notifier.events.connect (on_tracker_event); } @@ -30,10 +32,10 @@ public class Music.LibraryManager : Object { try { var tracker_statement = tracker_connection.query_statement ( """ - SELECT ?url ?title ?artist ?duration + SELECT ?url ?title ?artist ?duration ?id WHERE { GRAPH tracker:Audio { - SELECT ?url ?title ?artist ?duration + SELECT ?url ?title ?artist ?duration ?song AS ?id WHERE { ?song a nmm:MusicPiece ; nie:isStoredAs ?url . @@ -71,6 +73,8 @@ public class Music.LibraryManager : Object { audio_object.duration = cursor.get_integer (3); } + songs_by_urn[cursor.get_string (4)] = audio_object; + songs.append (audio_object); } @@ -80,24 +84,106 @@ public class Music.LibraryManager : Object { } } - private void on_tracker_event (string service, string graph, GenericArray events) { - print (service); - print (graph); + private void on_tracker_event (string? service, string? graph, GenericArray events) { foreach (var event in events) { + if (event.get_urn () == null) { + continue; + } + var type = event.get_event_type (); switch (type) { case DELETE: - print ("FILE DELETED"); + var audio_object = songs_by_urn[event.get_urn ()]; + if (audio_object != null) { + uint position = Gtk.INVALID_LIST_POSITION; + if (songs.find_with_equal_func (audio_object, equal_func, out position)) { + songs.remove (position); + } + } break; case CREATE: - print ("FILE CREATED"); + update_audio_object (event.get_urn ()); break; case UPDATE: - print ("FILE UPDATED"); + update_audio_object (event.get_urn ()); break; } } } + + private void update_audio_object (string urn) { + try { + var tracker_statement = tracker_connection.query_statement ( + """ + SELECT ?url ?title ?artist ?duration + WHERE { + GRAPH tracker:Audio { + SELECT ?url ?title ?artist ?duration + WHERE { + ~urn nie:isStoredAs ?url . + OPTIONAL { + ~urn nie:title ?title + } . + OPTIONAL { + ~urn nmm:artist [ nmm:artistName ?artist ] ; + } . + OPTIONAL { + ~urn nfo:duration ?duration ; + } . + } + } + } + """ + ); + + tracker_statement.bind_string ("urn", urn); + + var cursor = tracker_statement.execute (null); + + while (cursor.next ()) { + uint position = Gtk.INVALID_LIST_POSITION; + bool found = false; + AudioObject audio_object; + + if (!(urn in songs_by_urn)) { + audio_object = new AudioObject (cursor.get_string (0)); + } else { + audio_object = songs_by_urn[urn]; + found = songs.find_with_equal_func (audio_object, equal_func, out position); + + audio_object.uri = cursor.get_string (0); + } + + if (cursor.is_bound (1)) { + audio_object.title = cursor.get_string (1); + } else { + audio_object.title = audio_object.uri; + } + + if (cursor.is_bound (2)) { + audio_object.artist = cursor.get_string (2); + } + + if (cursor.is_bound (3)) { + audio_object.duration = cursor.get_integer (3); + } + + if (found) { + songs.items_changed (position, 1, 1); + } else { + songs.append (audio_object); + } + } + + cursor.close (); + } catch (Error e) { + warning (e.message); + } + } + + private static bool equal_func (Object a, Object b) { + return ((AudioObject) a).uri == ((AudioObject) b).uri; + } } From da2a2c9718e0f4b91f4c0b05a6d26ead2e44927f Mon Sep 17 00:00:00 2001 From: Leonhard Date: Mon, 24 Jul 2023 23:25:32 +0200 Subject: [PATCH 18/27] Tracker works really good now --- src/AudioObject.vala | 6 +- src/Services/LibraryManager.vala | 172 ++++++++++++++---------------- src/Services/PlaybackManager.vala | 4 +- 3 files changed, 84 insertions(+), 98 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index 1d968897d..8d0fe81e4 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -10,12 +10,8 @@ public class Music.AudioObject : Object { public string title { get; set; } public int64 duration { get; set; default = 0; } - public AudioObject (string uri) { - Object (uri: uri); - } - public AudioObject.from_file (File file) { - base (file.get_uri ()); + uri = file.get_uri (); string? basename = file.get_basename (); diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 9fcc9b7d1..86e50e025 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -29,100 +29,79 @@ public class Music.LibraryManager : Object { } private async void get_audio_files () { - try { - var tracker_statement = tracker_connection.query_statement ( - """ - SELECT ?url ?title ?artist ?duration ?id - WHERE { - GRAPH tracker:Audio { - SELECT ?url ?title ?artist ?duration ?song AS ?id - WHERE { - ?song a nmm:MusicPiece ; - nie:isStoredAs ?url . - OPTIONAL { - ?song nie:title ?title - } . - OPTIONAL { - ?song nmm:artist [ nmm:artistName ?artist ] ; - } . - OPTIONAL { - ?song nfo:duration ?duration ; - } . + new Thread (null, () => { + try { + var tracker_statement = tracker_connection.query_statement ( + """ + SELECT ?urn ?url ?title ?artist ?duration + WHERE { + GRAPH tracker:Audio { + SELECT ?song AS ?urn ?url ?title ?artist ?duration + WHERE { + ?song a nmm:MusicPiece ; + nie:isStoredAs ?url . + OPTIONAL { + ?song nie:title ?title + } . + OPTIONAL { + ?song nmm:artist [ nmm:artistName ?artist ] ; + } . + OPTIONAL { + ?song nfo:duration ?duration ; + } . + } } } - } - """ - ); - - var cursor = tracker_statement.execute (null); + """ + ); - while (cursor.next ()) { - var audio_object = new AudioObject (cursor.get_string (0)); + var cursor = tracker_statement.execute (null); - if (cursor.is_bound (1)) { - audio_object.title = cursor.get_string (1); - } else { - audio_object.title = audio_object.uri; + while (cursor.next ()) { + create_audio_object (cursor, false); } - if (cursor.is_bound (2)) { - audio_object.artist = cursor.get_string (2); - } - - if (cursor.is_bound (3)) { - audio_object.duration = cursor.get_integer (3); - } - - songs_by_urn[cursor.get_string (4)] = audio_object; - - songs.append (audio_object); + cursor.close (); + } catch (Error e) { + warning (e.message); } - cursor.close (); - } catch (Error e) { - warning (e.message); - } + Idle.add (get_audio_files.callback); + return null; + }); + + yield; } private void on_tracker_event (string? service, string? graph, GenericArray events) { foreach (var event in events) { - if (event.get_urn () == null) { - continue; - } - var type = event.get_event_type (); switch (type) { case DELETE: - var audio_object = songs_by_urn[event.get_urn ()]; - if (audio_object != null) { - uint position = Gtk.INVALID_LIST_POSITION; - if (songs.find_with_equal_func (audio_object, equal_func, out position)) { - songs.remove (position); - } - } break; case CREATE: - update_audio_object (event.get_urn ()); + query_update_audio_object (event.get_urn (), false); break; case UPDATE: - update_audio_object (event.get_urn ()); + query_update_audio_object (event.get_urn (), true); break; } } } - private void update_audio_object (string urn) { + private void query_update_audio_object (string urn, bool update) { try { var tracker_statement = tracker_connection.query_statement ( """ - SELECT ?url ?title ?artist ?duration + SELECT ~urn ?url ?title ?artist ?duration WHERE { GRAPH tracker:Audio { - SELECT ?url ?title ?artist ?duration + SELECT ~urn ?url ?title ?artist ?duration WHERE { - ~urn nie:isStoredAs ?url . + ~urn a nmm:MusicPiece ; + nie:isStoredAs ?url . OPTIONAL { ~urn nie:title ?title } . @@ -143,43 +122,52 @@ public class Music.LibraryManager : Object { var cursor = tracker_statement.execute (null); while (cursor.next ()) { - uint position = Gtk.INVALID_LIST_POSITION; - bool found = false; - AudioObject audio_object; + create_audio_object (cursor, update); + } - if (!(urn in songs_by_urn)) { - audio_object = new AudioObject (cursor.get_string (0)); - } else { - audio_object = songs_by_urn[urn]; - found = songs.find_with_equal_func (audio_object, equal_func, out position); + cursor.close (); + } catch (Error e) { + warning (e.message); + } + } - audio_object.uri = cursor.get_string (0); - } + private void create_audio_object (Tracker.Sparql.Cursor cursor, bool update = false) { + var urn = cursor.get_string (0); - if (cursor.is_bound (1)) { - audio_object.title = cursor.get_string (1); - } else { - audio_object.title = audio_object.uri; - } + AudioObject? audio_object = songs_by_urn[urn]; - if (cursor.is_bound (2)) { - audio_object.artist = cursor.get_string (2); - } + uint position = Gtk.INVALID_LIST_POSITION; + bool found = false; - if (cursor.is_bound (3)) { - audio_object.duration = cursor.get_integer (3); - } + if (audio_object == null) { + audio_object = new AudioObject (); + } else if (!update) { + return; + } else { + found = songs.find_with_equal_func (audio_object, equal_func, out position); + } - if (found) { - songs.items_changed (position, 1, 1); - } else { - songs.append (audio_object); - } - } + audio_object.uri = cursor.get_string (1); - cursor.close (); - } catch (Error e) { - warning (e.message); + if (cursor.is_bound (2)) { + audio_object.title = cursor.get_string (2); + } else { + audio_object.title = audio_object.uri; //TODO: Try basename, only then use URI + } + + if (cursor.is_bound (3)) { + audio_object.artist = cursor.get_string (3); + } + + if (cursor.is_bound (4)) { + audio_object.duration = cursor.get_integer (4); + } + + if (found) { + songs.items_changed (position, 1, 1); + } else { + songs.append (audio_object); + songs_by_urn[urn] = audio_object; } } diff --git a/src/Services/PlaybackManager.vala b/src/Services/PlaybackManager.vala index 1e80ae15c..684033466 100644 --- a/src/Services/PlaybackManager.vala +++ b/src/Services/PlaybackManager.vala @@ -158,7 +158,9 @@ public class Music.PlaybackManager : Object { return ((AudioObject) a).uri == ((AudioObject) b).uri; }; - var temp_audio_object = new AudioObject (uri); + var temp_audio_object = new AudioObject () { + uri = uri + }; uint position = -1; queue_liststore.find_with_equal_func (temp_audio_object, equal_func, out position); From 3c0359ad49699b0a477e41b025dc713fe5c308e5 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 00:11:16 +0200 Subject: [PATCH 19/27] Don't thread manually --- src/Services/LibraryManager.vala | 65 ++++++++++++++------------------ 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 86e50e025..18309fca4 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -29,48 +29,41 @@ public class Music.LibraryManager : Object { } private async void get_audio_files () { - new Thread (null, () => { - try { - var tracker_statement = tracker_connection.query_statement ( - """ - SELECT ?urn ?url ?title ?artist ?duration - WHERE { - GRAPH tracker:Audio { - SELECT ?song AS ?urn ?url ?title ?artist ?duration - WHERE { - ?song a nmm:MusicPiece ; - nie:isStoredAs ?url . - OPTIONAL { - ?song nie:title ?title - } . - OPTIONAL { - ?song nmm:artist [ nmm:artistName ?artist ] ; - } . - OPTIONAL { - ?song nfo:duration ?duration ; - } . - } + try { + var tracker_statement = tracker_connection.query_statement ( + """ + SELECT ?urn ?url ?title ?artist ?duration + WHERE { + GRAPH tracker:Audio { + SELECT ?song AS ?urn ?url ?title ?artist ?duration + WHERE { + ?song a nmm:MusicPiece ; + nie:isStoredAs ?url . + OPTIONAL { + ?song nie:title ?title + } . + OPTIONAL { + ?song nmm:artist [ nmm:artistName ?artist ] ; + } . + OPTIONAL { + ?song nfo:duration ?duration ; + } . } } - """ - ); - - var cursor = tracker_statement.execute (null); + } + """ + ); - while (cursor.next ()) { - create_audio_object (cursor, false); - } + var cursor = yield tracker_statement.execute_async (null); - cursor.close (); - } catch (Error e) { - warning (e.message); + while (cursor.next ()) { + create_audio_object (cursor, false); } - Idle.add (get_audio_files.callback); - return null; - }); - - yield; + cursor.close (); + } catch (Error e) { + warning (e.message); + } } private void on_tracker_event (string? service, string? graph, GenericArray events) { From bbf58e6dedbf7e1a6d8901a01e9be9a0750e2fef Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 00:37:59 +0200 Subject: [PATCH 20/27] Workaround tracker bug --- src/Services/LibraryManager.vala | 65 +++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 18309fca4..00c1a5c6b 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -30,37 +30,58 @@ public class Music.LibraryManager : Object { private async void get_audio_files () { try { - var tracker_statement = tracker_connection.query_statement ( + // There currently is a bug in tracker that from a flatpak large queries will stall indefinitely. + // Therefore we query all URN's and do separate queries for the details of each URN + // This will cost us quite a bit of performance which shoudln't be visible thought + // as it only leads to the library filling bit by bit but doesn't block anything + // Tested with Ryzen 5 3600 and about 600 Songs it took 1/2 second to fully load + var tracker_statement_urn = tracker_connection.query_statement ( """ - SELECT ?urn ?url ?title ?artist ?duration + SELECT ?urn WHERE { GRAPH tracker:Audio { - SELECT ?song AS ?urn ?url ?title ?artist ?duration + SELECT ?song AS ?urn WHERE { - ?song a nmm:MusicPiece ; - nie:isStoredAs ?url . - OPTIONAL { - ?song nie:title ?title - } . - OPTIONAL { - ?song nmm:artist [ nmm:artistName ?artist ] ; - } . - OPTIONAL { - ?song nfo:duration ?duration ; - } . + ?song a nmm:MusicPiece . } } } """ ); - var cursor = yield tracker_statement.execute_async (null); + var urn_cursor = yield tracker_statement_urn.execute_async (null); - while (cursor.next ()) { - create_audio_object (cursor, false); + while (yield urn_cursor.next_async ()) { + yield query_update_audio_object (urn_cursor.get_string (0), false); } - cursor.close (); + urn_cursor.close (); + + // This would be the correct query: + + // var tracker_statement = tracker_connection.query_statement ( + // """ + // SELECT ?urn ?url ?title ?artist ?duration + // WHERE { + // GRAPH tracker:Audio { + // SELECT ?song AS ?urn ?url ?title ?artist ?duration + // WHERE { + // ?song a nmm:MusicPiece ; + // nie:isStoredAs ?url . + // OPTIONAL { + // ?song nie:title ?title + // } . + // OPTIONAL { + // ?song nmm:artist [ nmm:artistName ?artist ] ; + // } . + // OPTIONAL { + // ?song nfo:duration ?duration ; + // } . + // } + // } + // } + // """ + // ); } catch (Error e) { warning (e.message); } @@ -74,17 +95,17 @@ public class Music.LibraryManager : Object { break; case CREATE: - query_update_audio_object (event.get_urn (), false); + query_update_audio_object.begin (event.get_urn (), false); break; case UPDATE: - query_update_audio_object (event.get_urn (), true); + query_update_audio_object.begin (event.get_urn (), true); break; } } } - private void query_update_audio_object (string urn, bool update) { + private async void query_update_audio_object (string urn, bool update) { try { var tracker_statement = tracker_connection.query_statement ( """ @@ -112,7 +133,7 @@ public class Music.LibraryManager : Object { tracker_statement.bind_string ("urn", urn); - var cursor = tracker_statement.execute (null); + var cursor = yield tracker_statement.execute_async (null); while (cursor.next ()) { create_audio_object (cursor, update); From ba600fc85bd502ca0a8cf15f1aa9d436ddd062d9 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 16:29:09 +0200 Subject: [PATCH 21/27] Fix queue crash and deletion is now refelected --- src/Services/LibraryManager.vala | 66 +++++++++++++++++++------------- src/Views/QueueView.vala | 2 +- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 00c1a5c6b..55e2d37d3 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -3,7 +3,7 @@ public class Music.LibraryManager : Object { private Tracker.Sparql.Connection tracker_connection; private Tracker.Notifier notifier; - private HashTable songs_by_urn; + private HashTable songs_by_id; private static GLib.Once instance; public static unowned LibraryManager get_instance () { @@ -12,7 +12,7 @@ public class Music.LibraryManager : Object { construct { songs = new ListStore (typeof (AudioObject)); - songs_by_urn = new HashTable (str_hash, str_equal); + songs_by_id = new HashTable (str_hash, str_equal); try { tracker_connection = Tracker.Sparql.Connection.bus_new ("org.freedesktop.Tracker3.Miner.Files", null, null); @@ -37,7 +37,7 @@ public class Music.LibraryManager : Object { // Tested with Ryzen 5 3600 and about 600 Songs it took 1/2 second to fully load var tracker_statement_urn = tracker_connection.query_statement ( """ - SELECT ?urn + SELECT tracker:id(?urn) WHERE { GRAPH tracker:Audio { SELECT ?song AS ?urn @@ -52,7 +52,7 @@ public class Music.LibraryManager : Object { var urn_cursor = yield tracker_statement_urn.execute_async (null); while (yield urn_cursor.next_async ()) { - yield query_update_audio_object (urn_cursor.get_string (0), false); + yield query_update_audio_object (urn_cursor.get_integer (0), false); } urn_cursor.close (); @@ -92,51 +92,63 @@ public class Music.LibraryManager : Object { var type = event.get_event_type (); switch (type) { case DELETE: + var id = event.get_id ().to_string (); + var audio_object = songs_by_id[id]; + + if (audio_object != null) { + songs_by_id.remove (id); + + uint position = Gtk.INVALID_LIST_POSITION; + if (songs.find_with_equal_func (audio_object, equal_func, out position)) { + songs.remove (position); + } + } break; case CREATE: - query_update_audio_object.begin (event.get_urn (), false); + query_update_audio_object.begin (event.get_id (), false); break; case UPDATE: - query_update_audio_object.begin (event.get_urn (), true); + query_update_audio_object.begin (event.get_id (), true); break; } } } - private async void query_update_audio_object (string urn, bool update) { + private async void query_update_audio_object (int64 id, bool update) { try { var tracker_statement = tracker_connection.query_statement ( """ - SELECT ~urn ?url ?title ?artist ?duration + SELECT ?url ?title ?artist ?duration WHERE { GRAPH tracker:Audio { - SELECT ~urn ?url ?title ?artist ?duration + SELECT ?song ?url ?title ?artist ?duration WHERE { - ~urn a nmm:MusicPiece ; - nie:isStoredAs ?url . + ?song a nmm:MusicPiece ; + nie:isStoredAs ?url . OPTIONAL { - ~urn nie:title ?title + ?song nie:title ?title } . OPTIONAL { - ~urn nmm:artist [ nmm:artistName ?artist ] ; + ?song nmm:artist [ nmm:artistName ?artist ] ; } . OPTIONAL { - ~urn nfo:duration ?duration ; + ?song nfo:duration ?duration ; } . + FILTER(tracker:id (?song) = ~id) } } } """ ); - tracker_statement.bind_string ("urn", urn); + tracker_statement.bind_int ("id", id); var cursor = yield tracker_statement.execute_async (null); while (cursor.next ()) { - create_audio_object (cursor, update); + create_audio_object (id, cursor, update); } cursor.close (); @@ -145,10 +157,10 @@ public class Music.LibraryManager : Object { } } - private void create_audio_object (Tracker.Sparql.Cursor cursor, bool update = false) { - var urn = cursor.get_string (0); + private void create_audio_object (int64 _id, Tracker.Sparql.Cursor cursor, bool update = false) { + var id = _id.to_string (); - AudioObject? audio_object = songs_by_urn[urn]; + AudioObject? audio_object = songs_by_id[id]; uint position = Gtk.INVALID_LIST_POSITION; bool found = false; @@ -161,27 +173,27 @@ public class Music.LibraryManager : Object { found = songs.find_with_equal_func (audio_object, equal_func, out position); } - audio_object.uri = cursor.get_string (1); + audio_object.uri = cursor.get_string (0); - if (cursor.is_bound (2)) { - audio_object.title = cursor.get_string (2); + if (cursor.is_bound (1)) { + audio_object.title = cursor.get_string (1); } else { audio_object.title = audio_object.uri; //TODO: Try basename, only then use URI } - if (cursor.is_bound (3)) { - audio_object.artist = cursor.get_string (3); + if (cursor.is_bound (2)) { + audio_object.artist = cursor.get_string (2); } - if (cursor.is_bound (4)) { - audio_object.duration = cursor.get_integer (4); + if (cursor.is_bound (3)) { + audio_object.duration = cursor.get_integer (3); } if (found) { songs.items_changed (position, 1, 1); } else { songs.append (audio_object); - songs_by_urn[urn] = audio_object; + songs_by_id[id] = audio_object; } } diff --git a/src/Views/QueueView.vala b/src/Views/QueueView.vala index 92a7866b0..7be441bf8 100644 --- a/src/Views/QueueView.vala +++ b/src/Views/QueueView.vala @@ -74,7 +74,7 @@ public class Music.QueueView : Gtk.Box { }); queue_listbox.row_activated.connect ((row) => { - playback_manager.current_audio = ((TrackRow) row).audio_object; + playback_manager.current_audio = ((TrackRow) row.child).audio_object; }); } From cc1a2af4e350e288f51ec15f022958b1430b86ad Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 16:52:09 +0200 Subject: [PATCH 22/27] Sort alphabeticly --- src/Services/LibraryManager.vala | 26 +++++++++++++++----------- src/Views/LibraryView.vala | 8 ++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 55e2d37d3..d28d7e9bf 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -31,11 +31,11 @@ public class Music.LibraryManager : Object { private async void get_audio_files () { try { // There currently is a bug in tracker that from a flatpak large queries will stall indefinitely. - // Therefore we query all URN's and do separate queries for the details of each URN + // Therefore we query all ID's and do separate queries for the details of each ID // This will cost us quite a bit of performance which shoudln't be visible thought // as it only leads to the library filling bit by bit but doesn't block anything // Tested with Ryzen 5 3600 and about 600 Songs it took 1/2 second to fully load - var tracker_statement_urn = tracker_connection.query_statement ( + var tracker_statement_id = tracker_connection.query_statement ( """ SELECT tracker:id(?urn) WHERE { @@ -49,22 +49,22 @@ public class Music.LibraryManager : Object { """ ); - var urn_cursor = yield tracker_statement_urn.execute_async (null); + var id_cursor = yield tracker_statement_id.execute_async (null); - while (yield urn_cursor.next_async ()) { - yield query_update_audio_object (urn_cursor.get_integer (0), false); + while (yield id_cursor.next_async ()) { + yield query_update_audio_object (id_cursor.get_integer (0), false); } - urn_cursor.close (); + id_cursor.close (); - // This would be the correct query: + // This would be the actual query: // var tracker_statement = tracker_connection.query_statement ( // """ - // SELECT ?urn ?url ?title ?artist ?duration + // SELECT ?url ?title ?artist ?duration // WHERE { // GRAPH tracker:Audio { - // SELECT ?song AS ?urn ?url ?title ?artist ?duration + // SELECT ?url ?title ?artist ?duration // WHERE { // ?song a nmm:MusicPiece ; // nie:isStoredAs ?url . @@ -158,7 +158,7 @@ public class Music.LibraryManager : Object { } private void create_audio_object (int64 _id, Tracker.Sparql.Cursor cursor, bool update = false) { - var id = _id.to_string (); + var id = _id.to_string (); //TODO: Maybe use the int64 directly as key AudioObject? audio_object = songs_by_id[id]; @@ -192,11 +192,15 @@ public class Music.LibraryManager : Object { if (found) { songs.items_changed (position, 1, 1); } else { - songs.append (audio_object); + songs.insert_sorted (audio_object, compare_func); songs_by_id[id] = audio_object; } } + private static int compare_func (Object a, Object b) { + return ((AudioObject) a).title.collate (((AudioObject) b).title); + } + private static bool equal_func (Object a, Object b) { return ((AudioObject) a).uri == ((AudioObject) b).uri; } diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index 88421d612..ebb4fb2ce 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -11,6 +11,8 @@ public class Music.LibraryView : Gtk.Box { icon = new ThemedIcon ("folder-music") }; + // var sort_model = new Gtk.SortListModel (library_manager.songs, new Gtk.CustomSorter (sort_func)); + selection_model = new Gtk.SingleSelection (library_manager.songs) { can_unselect = true, autoselect = false @@ -75,6 +77,12 @@ public class Music.LibraryView : Gtk.Box { }); } + // private static int sort_func (Object a, Object b) { + // var audio_object_a = (AudioObject)a; + // var audio_object_b = (AudioObject)b; + // return audio_object_a.title.collate (audio_object_b.title); + // } + private void update_stack () { placeholder_stack.visible_child_name = selection_model.get_n_items () > 0 ? "list-view" : "placeholder"; } From 8a3a78e7e8ec19b2de44c27134f31bae2e2896fa Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 17:15:48 +0200 Subject: [PATCH 23/27] Add a loading placeholder --- src/Services/LibraryManager.vala | 4 +--- src/Views/LibraryView.vala | 38 +++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index d28d7e9bf..6d136553f 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -21,14 +21,12 @@ public class Music.LibraryManager : Object { if (notifier != null) { notifier.events.connect (on_tracker_event); } - - get_audio_files.begin (); } catch (Error e) { warning (e.message); } } - private async void get_audio_files () { + public async void get_audio_files () { try { // There currently is a bug in tracker that from a flatpak large queries will stall indefinitely. // Therefore we query all ID's and do separate queries for the details of each ID diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index ebb4fb2ce..87e347594 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -11,7 +11,10 @@ public class Music.LibraryView : Gtk.Box { icon = new ThemedIcon ("folder-music") }; - // var sort_model = new Gtk.SortListModel (library_manager.songs, new Gtk.CustomSorter (sort_func)); + var loading_placeholder = new Granite.Placeholder (_("Loading Songs")) { + description = _("Looking for Audio files in your Music directory"), + icon = new ThemedIcon ("sync-synchronizing") + }; selection_model = new Gtk.SingleSelection (library_manager.songs) { can_unselect = true, @@ -33,10 +36,28 @@ public class Music.LibraryView : Gtk.Box { placeholder_stack = new Gtk.Stack (); placeholder_stack.add_named (scrolled_window, "list-view"); placeholder_stack.add_named (placeholder, "placeholder"); + placeholder_stack.add_named (loading_placeholder, "loading-placeholder"); + placeholder_stack.visible_child_name = "loading-placeholder"; + + var overlay = new Gtk.Overlay () { + child = placeholder_stack + }; - append (placeholder_stack); + var loading_overlay_bar = new Granite.OverlayBar (overlay) { + label = _("Discovering Songs"), + active = true + }; + + append (overlay); + + bool loading = true; + library_manager.get_audio_files.begin (() => { + loading_overlay_bar.visible = false; + loading = false; + update_stack (loading); + }); - selection_model.items_changed.connect (update_stack); + selection_model.items_changed.connect (() => update_stack (loading)); selection_model.selection_changed.connect (() => { //TODO: Should clear play queue? @@ -44,7 +65,6 @@ public class Music.LibraryView : Gtk.Box { }); selection_model.set_selected (Gtk.INVALID_LIST_POSITION); - update_stack (); playback_manager.ask_has_next.connect ((repeat_all) => { if (selection_model.get_n_items () == 0) { @@ -77,13 +97,11 @@ public class Music.LibraryView : Gtk.Box { }); } - // private static int sort_func (Object a, Object b) { - // var audio_object_a = (AudioObject)a; - // var audio_object_b = (AudioObject)b; - // return audio_object_a.title.collate (audio_object_b.title); - // } + private void update_stack (bool loading) { + if (loading) { + return; + } - private void update_stack () { placeholder_stack.visible_child_name = selection_model.get_n_items () > 0 ? "list-view" : "placeholder"; } From 43226a00e563bb218562d6a3f11990f93714e607 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 18:37:52 +0200 Subject: [PATCH 24/27] Use basename if no title was found + load images in library --- src/AudioObject.vala | 30 ++++++++++++++++++++++++++++++ src/Services/LibraryManager.vala | 6 +++++- src/Services/PlaybackManager.vala | 4 ++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index 8d0fe81e4..0a2166341 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -21,4 +21,34 @@ public class Music.AudioObject : Object { title = uri; } } + + construct { + new Thread (null, () => { + try { + var discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND)); + + var info = discoverer.discover_uri (uri); + + if (info == null) { + warning ("Discovery failed."); + return null; + } + + unowned Gst.TagList? tag_list = info.get_tags (); + + var sample = PlaybackManager.get_cover_sample (tag_list); + if (sample != null) { + var buffer = sample.get_buffer (); + + if (buffer != null) { + texture = Gdk.Texture.for_pixbuf (PlaybackManager.get_pixbuf_from_buffer (buffer)); + } + } + } catch (Error e) { + warning ("Failed to create texture: %s", e.message); + } + + return null; + }); + } } diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 6d136553f..f9528bd6d 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -176,7 +176,11 @@ public class Music.LibraryManager : Object { if (cursor.is_bound (1)) { audio_object.title = cursor.get_string (1); } else { - audio_object.title = audio_object.uri; //TODO: Try basename, only then use URI + try { + audio_object.title = Filename.display_basename (Filename.from_uri (audio_object.uri)); + } catch (Error e) { + audio_object.title = audio_object.uri; + } } if (cursor.is_bound (2)) { diff --git a/src/Services/PlaybackManager.vala b/src/Services/PlaybackManager.vala index 684033466..f649bb7e7 100644 --- a/src/Services/PlaybackManager.vala +++ b/src/Services/PlaybackManager.vala @@ -351,7 +351,7 @@ public class Music.PlaybackManager : Object { } - private Gst.Sample? get_cover_sample (Gst.TagList tag_list) { + public static Gst.Sample? get_cover_sample (Gst.TagList tag_list) { Gst.Sample cover_sample = null; Gst.Sample sample; for (int i = 0; tag_list.get_sample_index (Gst.Tags.IMAGE, i, out sample); i++) { @@ -369,7 +369,7 @@ public class Music.PlaybackManager : Object { return cover_sample; } - private Gdk.Pixbuf? get_pixbuf_from_buffer (Gst.Buffer buffer) { + public static Gdk.Pixbuf? get_pixbuf_from_buffer (Gst.Buffer buffer) { Gst.MapInfo map_info; if (!buffer.map (out map_info, Gst.MapFlags.READ)) { From 674f3cd394eb762e88ef5e723b0c96627f8584bf Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 25 Jul 2023 18:41:00 +0200 Subject: [PATCH 25/27] Don't abort when failing to create a thread --- src/AudioObject.vala | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index 0a2166341..adb40b9fc 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -23,32 +23,36 @@ public class Music.AudioObject : Object { } construct { - new Thread (null, () => { - try { - var discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND)); + try { + new Thread.try (null, () => { + try { + var discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND)); - var info = discoverer.discover_uri (uri); + var info = discoverer.discover_uri (uri); - if (info == null) { - warning ("Discovery failed."); - return null; - } + if (info == null) { + warning ("Discovery failed."); + return null; + } - unowned Gst.TagList? tag_list = info.get_tags (); + unowned Gst.TagList? tag_list = info.get_tags (); - var sample = PlaybackManager.get_cover_sample (tag_list); - if (sample != null) { - var buffer = sample.get_buffer (); + var sample = PlaybackManager.get_cover_sample (tag_list); + if (sample != null) { + var buffer = sample.get_buffer (); - if (buffer != null) { - texture = Gdk.Texture.for_pixbuf (PlaybackManager.get_pixbuf_from_buffer (buffer)); + if (buffer != null) { + texture = Gdk.Texture.for_pixbuf (PlaybackManager.get_pixbuf_from_buffer (buffer)); + } } + } catch (Error e) { + warning ("Failed to create texture: %s", e.message); } - } catch (Error e) { - warning ("Failed to create texture: %s", e.message); - } - return null; - }); + return null; + }); + } catch (Error e) { + warning ("Failed to create thread: %s", e.message); + } } } From 768c94e0fff8d1c2956a19136af2d02acb84c1d6 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 1 Aug 2023 18:31:06 +0200 Subject: [PATCH 26/27] Remove need for laoding placeholder --- src/Services/LibraryManager.vala | 12 +++++++----- src/Views/LibraryView.vala | 24 +++++------------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index f9528bd6d..2b18368e7 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -30,20 +30,22 @@ public class Music.LibraryManager : Object { try { // There currently is a bug in tracker that from a flatpak large queries will stall indefinitely. // Therefore we query all ID's and do separate queries for the details of each ID - // This will cost us quite a bit of performance which shoudln't be visible thought + // This will cost us quite a bit of performance which shouldn't be visible though // as it only leads to the library filling bit by bit but doesn't block anything - // Tested with Ryzen 5 3600 and about 600 Songs it took 1/2 second to fully load + // Tested with Ryzen 5 3600 and about 600 Songs it took half a second to fully load var tracker_statement_id = tracker_connection.query_statement ( """ - SELECT tracker:id(?urn) + SELECT tracker:id(?urn) ?url WHERE { GRAPH tracker:Audio { - SELECT ?song AS ?urn + SELECT ?song AS ?urn ?url WHERE { - ?song a nmm:MusicPiece . + ?song a nmm:MusicPiece ; + nie:isStoredAs ?url . } } } + ORDER BY ?url """ ); diff --git a/src/Views/LibraryView.vala b/src/Views/LibraryView.vala index 87e347594..3c1e2517a 100644 --- a/src/Views/LibraryView.vala +++ b/src/Views/LibraryView.vala @@ -11,11 +11,6 @@ public class Music.LibraryView : Gtk.Box { icon = new ThemedIcon ("folder-music") }; - var loading_placeholder = new Granite.Placeholder (_("Loading Songs")) { - description = _("Looking for Audio files in your Music directory"), - icon = new ThemedIcon ("sync-synchronizing") - }; - selection_model = new Gtk.SingleSelection (library_manager.songs) { can_unselect = true, autoselect = false @@ -36,8 +31,7 @@ public class Music.LibraryView : Gtk.Box { placeholder_stack = new Gtk.Stack (); placeholder_stack.add_named (scrolled_window, "list-view"); placeholder_stack.add_named (placeholder, "placeholder"); - placeholder_stack.add_named (loading_placeholder, "loading-placeholder"); - placeholder_stack.visible_child_name = "loading-placeholder"; + placeholder_stack.visible_child_name = "placeholder"; var overlay = new Gtk.Overlay () { child = placeholder_stack @@ -50,14 +44,10 @@ public class Music.LibraryView : Gtk.Box { append (overlay); - bool loading = true; - library_manager.get_audio_files.begin (() => { - loading_overlay_bar.visible = false; - loading = false; - update_stack (loading); - }); + library_manager.get_audio_files.begin (() => loading_overlay_bar.visible = false); - selection_model.items_changed.connect (() => update_stack (loading)); + selection_model.items_changed.connect (update_stack); + update_stack (); selection_model.selection_changed.connect (() => { //TODO: Should clear play queue? @@ -97,11 +87,7 @@ public class Music.LibraryView : Gtk.Box { }); } - private void update_stack (bool loading) { - if (loading) { - return; - } - + private void update_stack () { placeholder_stack.visible_child_name = selection_model.get_n_items () > 0 ? "list-view" : "placeholder"; } From f570074b4e3b18ab8eb218c24643eb7a729b76fb Mon Sep 17 00:00:00 2001 From: Leonhard Date: Tue, 1 Aug 2023 19:22:23 +0200 Subject: [PATCH 27/27] Move all metadata discovery to audioobject --- src/AudioObject.vala | 32 +++++++++- src/Services/LibraryManager.vala | 14 ++-- src/Services/PlaybackManager.vala | 102 ------------------------------ 3 files changed, 39 insertions(+), 109 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index adb40b9fc..fd30ba724 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -22,7 +22,7 @@ public class Music.AudioObject : Object { } } - construct { + public void update_metadata () { try { new Thread.try (null, () => { try { @@ -37,12 +37,38 @@ public class Music.AudioObject : Object { unowned Gst.TagList? tag_list = info.get_tags (); + duration = (int64) info.get_duration (); + + string _title; + tag_list.get_string (Gst.Tags.TITLE, out _title); + if (_title != null) { + title = _title; + } + + string _artist; + tag_list.get_string (Gst.Tags.ARTIST, out _artist); + if (_artist != null) { + artist = _artist; + } else if (_title != null) { // Don't set artist for files without tags + artist = _("Unknown"); + } + var sample = PlaybackManager.get_cover_sample (tag_list); if (sample != null) { var buffer = sample.get_buffer (); - if (buffer != null) { - texture = Gdk.Texture.for_pixbuf (PlaybackManager.get_pixbuf_from_buffer (buffer)); + Gst.MapInfo? map_info = null; + if (buffer != null && buffer.map (out map_info, Gst.MapFlags.READ) && map_info != null) { + var bytes = new Bytes (map_info.data); + try { + texture = Gdk.Texture.from_bytes (bytes); + } catch (Error e) { + warning ("Error processing image data: %s", e.message); + } + + buffer.unmap (map_info); + } else { + warning ("Could not map memory buffer"); } } } catch (Error e) { diff --git a/src/Services/LibraryManager.vala b/src/Services/LibraryManager.vala index 2b18368e7..8c2c9fe36 100644 --- a/src/Services/LibraryManager.vala +++ b/src/Services/LibraryManager.vala @@ -35,17 +35,17 @@ public class Music.LibraryManager : Object { // Tested with Ryzen 5 3600 and about 600 Songs it took half a second to fully load var tracker_statement_id = tracker_connection.query_statement ( """ - SELECT tracker:id(?urn) ?url + SELECT tracker:id(?urn) ?sort_prop WHERE { GRAPH tracker:Audio { - SELECT ?song AS ?urn ?url + SELECT ?song AS ?urn ?sort_prop WHERE { ?song a nmm:MusicPiece ; - nie:isStoredAs ?url . + nie:isStoredAs ?sort_prop . } } } - ORDER BY ?url + ORDER BY ?sort_prop """ ); @@ -175,6 +175,10 @@ public class Music.LibraryManager : Object { audio_object.uri = cursor.get_string (0); + // We set the following properties although they are set anyway in + // update_metadata. We do this because update_metadata takes a while + // and we want the ui to already show something + if (cursor.is_bound (1)) { audio_object.title = cursor.get_string (1); } else { @@ -193,6 +197,8 @@ public class Music.LibraryManager : Object { audio_object.duration = cursor.get_integer (3); } + audio_object.update_metadata (); + if (found) { songs.items_changed (position, 1, 1); } else { diff --git a/src/Services/PlaybackManager.vala b/src/Services/PlaybackManager.vala index f649bb7e7..7fe748fea 100644 --- a/src/Services/PlaybackManager.vala +++ b/src/Services/PlaybackManager.vala @@ -18,7 +18,6 @@ public class Music.PlaybackManager : Object { } private dynamic Gst.Element playbin; - private Gst.PbUtils.Discoverer discoverer; private uint progress_timer = 0; private Settings settings; @@ -43,14 +42,6 @@ public class Music.PlaybackManager : Object { bus.add_watch (0, bus_callback); bus.enable_sync_message_emission (); - try { - discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND)); - discoverer.discovered.connect (update_metadata); - discoverer.finished.connect (discoverer.stop); - } catch (Error e) { - critical ("Unable to start Gstreamer Discoverer: %s", e.message); - } - queue_liststore.items_changed.connect (() => { var shuffle_action_action = (SimpleAction) GLib.Application.get_default ().lookup_action (Application.ACTION_SHUFFLE); shuffle_action_action.set_enabled (queue_liststore.get_n_items () > 1); @@ -88,13 +79,11 @@ public class Music.PlaybackManager : Object { // Files[] must not contain any null entries public void queue_files (File[] files) { - discoverer.start (); int invalids = 0; foreach (unowned var file in files) { if (file.query_exists () && "audio" in ContentType.guess (file.get_uri (), null, null)) { var audio_object = new AudioObject.from_file (file); queue_liststore.append (audio_object); - discoverer.discover_uri_async (audio_object.uri); } else { invalids++; continue; @@ -132,72 +121,6 @@ public class Music.PlaybackManager : Object { } } - private void update_metadata (Gst.PbUtils.DiscovererInfo info, Error? err) { - string uri = info.get_uri (); - switch (info.get_result ()) { - case Gst.PbUtils.DiscovererResult.URI_INVALID: - critical ("Couldn't read metadata for '%s': invalid URI.", uri); - return; - case Gst.PbUtils.DiscovererResult.ERROR: - critical ("Couldn't read metadata for '%s': %s", uri, err.message); - return; - case Gst.PbUtils.DiscovererResult.TIMEOUT: - critical ("Couldn't read metadata for '%s': Discovery timed out.", uri); - return; - case Gst.PbUtils.DiscovererResult.BUSY: - critical ("Couldn't read metadata for '%s': Already discovering a file.", uri); - return; - case Gst.PbUtils.DiscovererResult.MISSING_PLUGINS: - critical ("Couldn't read metadata for '%s': Missing plugins.", uri); - return; - default: - break; - } - - EqualFunc equal_func = (a, b) => { - return ((AudioObject) a).uri == ((AudioObject) b).uri; - }; - - var temp_audio_object = new AudioObject () { - uri = uri - }; - - uint position = -1; - queue_liststore.find_with_equal_func (temp_audio_object, equal_func, out position); - - if (position != -1) { - var audio_object = (AudioObject) queue_liststore.get_item (position); - audio_object.duration = (int64) info.get_duration (); - - unowned Gst.TagList? tag_list = info.get_tags (); - - string _title; - tag_list.get_string (Gst.Tags.TITLE, out _title); - if (_title != null) { - audio_object.title = _title; - } - - string _artist; - tag_list.get_string (Gst.Tags.ARTIST, out _artist); - if (_artist != null) { - audio_object.artist = _artist; - } else if (_title != null) { // Don't set artist for files without tags - audio_object.artist = _("Unknown"); - } - - var sample = get_cover_sample (tag_list); - if (sample != null) { - var buffer = sample.get_buffer (); - - if (buffer != null) { - audio_object.texture = Gdk.Texture.for_pixbuf (get_pixbuf_from_buffer (buffer)); - } - } - } else { - critical ("Couldn't find '%s' in queue", uri); - } - } - private bool bus_callback (Gst.Bus bus, Gst.Message message) { switch (message.type) { case Gst.MessageType.EOS: @@ -368,29 +291,4 @@ public class Music.PlaybackManager : Object { return cover_sample; } - - public static Gdk.Pixbuf? get_pixbuf_from_buffer (Gst.Buffer buffer) { - Gst.MapInfo map_info; - - if (!buffer.map (out map_info, Gst.MapFlags.READ)) { - warning ("Could not map memory buffer"); - return null; - } - - Gdk.Pixbuf pix = null; - - try { - var loader = new Gdk.PixbufLoader (); - - if (loader.write (map_info.data) && loader.close ()) { - pix = loader.get_pixbuf (); - } - } catch (Error err) { - warning ("Error processing image data: %s", err.message); - } - - buffer.unmap (map_info); - - return pix; - } }