From afd600b3d31807465cd1df1ab7b0abb64097dfa5 Mon Sep 17 00:00:00 2001 From: oowoosh0 Date: Mon, 17 Mar 2025 21:15:24 +0100 Subject: [PATCH 1/3] Handle metadata discovery at creation of AudioObjects --- src/AudioObject.vala | 82 ++++++++++++++++++++++- src/MetadataDiscoverer.vala | 49 ++++++++++++++ src/PlaybackManager.vala | 127 ------------------------------------ src/meson.build | 1 + 4 files changed, 131 insertions(+), 128 deletions(-) create mode 100644 src/MetadataDiscoverer.vala diff --git a/src/AudioObject.vala b/src/AudioObject.vala index b80934a9d..42d802bf9 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -9,8 +9,88 @@ public class Music.AudioObject : Object { public string artist { get; set; } public string title { get; set; } public int64 duration { get; set; default = 0; } + private static MetadataDiscoverer discoverer = new MetadataDiscoverer(); public AudioObject (string uri) { - Object (uri: uri); + Object ( + uri: uri, + title: uri + ); + } + + construct { + discoverer.request (this); + } + + public void update_metadata (Gst.PbUtils.DiscovererInfo info) { + 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) { + 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 = get_cover_sample (tag_list); + if (sample != null) { + var buffer = sample.get_buffer (); + + if (buffer != null) { + texture = Gdk.Texture.for_pixbuf (get_pixbuf_from_buffer (buffer)); + } + } + } + + private 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++) { + var caps = sample.get_caps (); + unowned Gst.Structure caps_struct = caps.get_structure (0); + int image_type = Gst.Tag.ImageType.UNDEFINED; + caps_struct.get_enum ("image-type", typeof (Gst.Tag.ImageType), out image_type); + if (image_type == Gst.Tag.ImageType.UNDEFINED && cover_sample == null) { + cover_sample = sample; + } else if (image_type == Gst.Tag.ImageType.FRONT_COVER) { + return sample; + } + } + + return cover_sample; + } + + private 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; } } diff --git a/src/MetadataDiscoverer.vala b/src/MetadataDiscoverer.vala new file mode 100644 index 000000000..8b8f46a8b --- /dev/null +++ b/src/MetadataDiscoverer.vala @@ -0,0 +1,49 @@ +[SingleInstance] +public class Music.MetadataDiscoverer : Object { + private Gst.PbUtils.Discoverer discoverer; + private HashTable objects_to_update; + + construct { + try { + discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND)); + discoverer.discovered.connect (relay_metadata); + discoverer.finished.connect (discoverer.stop); + } catch (Error e) { + critical ("Unable to start Gstreamer Discoverer: %s", e.message); + } + objects_to_update = new HashTable (str_hash, str_equal); + } + + public void request (AudioObject audio) { + objects_to_update.insert (audio.uri, audio); + discoverer.start (); + discoverer.discover_uri_async (audio.uri); + } + + private void relay_metadata (Gst.PbUtils.DiscovererInfo info, Error? err) { + string uri = info.get_uri (); + var audio_obj = objects_to_update.get (uri); + objects_to_update.remove (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; + } + + audio_obj.update_metadata (info); + } +} diff --git a/src/PlaybackManager.vala b/src/PlaybackManager.vala index 117c8ab69..bc5014923 100644 --- a/src/PlaybackManager.vala +++ b/src/PlaybackManager.vala @@ -21,7 +21,6 @@ public class Music.PlaybackManager : Object { } private dynamic Gst.Element playbin; - private Gst.PbUtils.Discoverer discoverer; private uint progress_timer = 0; private Settings settings; @@ -46,14 +45,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); has_items = queue_liststore.get_n_items () > 0; @@ -91,22 +82,10 @@ 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 (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); - queue_liststore.append (audio_object); } else { invalids++; @@ -151,69 +130,6 @@ public class Music.PlaybackManager : Object { queue_liststore.remove_all (); } - 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); - - 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) { @@ -397,47 +313,4 @@ public class Music.PlaybackManager : Object { previous_action.set_enabled (previous_sensitive); } - - private 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++) { - var caps = sample.get_caps (); - unowned Gst.Structure caps_struct = caps.get_structure (0); - int image_type = Gst.Tag.ImageType.UNDEFINED; - caps_struct.get_enum ("image-type", typeof (Gst.Tag.ImageType), out image_type); - if (image_type == Gst.Tag.ImageType.UNDEFINED && cover_sample == null) { - cover_sample = sample; - } else if (image_type == Gst.Tag.ImageType.FRONT_COVER) { - return sample; - } - } - - return cover_sample; - } - - private 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; - } } diff --git a/src/meson.build b/src/meson.build index e4b85f439..de5a07916 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,6 +3,7 @@ sources = [ 'AudioObject.vala', 'MainWindow.vala', 'PlaybackManager.vala', + 'MetadataDiscoverer.vala', 'DBus/MprisPlayer.vala', 'DBus/MprisRoot.vala', 'Views/NowPlayingView.vala', From ca7df3e3abe78b704444d2ad42d313cb61ebf473 Mon Sep 17 00:00:00 2001 From: oowoosh0 Date: Thu, 20 Mar 2025 17:00:09 +0100 Subject: [PATCH 2/3] Fix lint error --- src/AudioObject.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index 42d802bf9..1ccf10da3 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -9,7 +9,7 @@ public class Music.AudioObject : Object { public string artist { get; set; } public string title { get; set; } public int64 duration { get; set; default = 0; } - private static MetadataDiscoverer discoverer = new MetadataDiscoverer(); + private static MetadataDiscoverer discoverer = new MetadataDiscoverer (); public AudioObject (string uri) { Object ( From d803fc2222dde7789530afc42e7bf740ebb07d10 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 19 Nov 2025 12:37:14 +0100 Subject: [PATCH 3/3] Fix merge --- src/AudioObject.vala | 67 ++++++++++++++++++++++++-------- src/MetadataDiscoverer.vala | 17 +++++--- src/PlaybackManager.vala | 77 +++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 20 deletions(-) diff --git a/src/AudioObject.vala b/src/AudioObject.vala index e127793e4..75305a07d 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -5,23 +5,21 @@ public class Music.AudioObject : Object { public string uri { get; construct; } - public Gdk.Texture? texture { get; set; default = null; } - public string album { get; set; } - public string artist { get; set; } - public string title { get; set; } - public int64 duration { get; set; default = 0; } - public string art_url { get; set; default = ""; } + public Gdk.Texture? texture { get; private set; default = null; } + public string album { get; private set; } + public string artist { get; private set; } + public string title { get; private set; } + public int64 duration { get; private set; default = 0; } + public string art_url { get; private set; default = ""; } private static MetadataDiscoverer discoverer = new MetadataDiscoverer (); public AudioObject (string uri) { - Object ( - uri: uri, - title: uri - ); + Object (uri: uri); } construct { + title = uri; discoverer.request (this); } @@ -44,12 +42,29 @@ public class Music.AudioObject : Object { artist = _("Unknown"); } - var sample = get_cover_sample (tag_list); - if (sample != null) { - var buffer = sample.get_buffer (); + string art_hash = uri; + if (_artist != null && _album != null) { + art_hash = "%s:%s".printf (_artist, _album); + } - if (buffer != null) { - texture = Gdk.Texture.for_pixbuf (get_pixbuf_from_buffer (buffer)); + var art_file = File.new_for_path (Path.build_path ( + Path.DIR_SEPARATOR_S, + get_art_cache_dir (), + Checksum.compute_for_string (SHA256, art_hash) + )); + + if (art_file.query_exists ()) { + art_url = art_file.get_uri (); + texture = Gdk.Texture.from_file (art_file); + } else { + var sample = get_cover_sample (tag_list); + if (sample != null) { + var buffer = sample.get_buffer (); + + if (buffer != null) { + texture = Gdk.Texture.for_pixbuf (get_pixbuf_from_buffer (buffer)); + save_art_file.begin (texture, art_file); + } } } } @@ -97,6 +112,28 @@ public class Music.AudioObject : Object { return pix; } + private async void save_art_file (Gdk.Texture? texture, File file) requires (texture != null) { + try { + DirUtils.create_with_parents (get_art_cache_dir (), 0755); + + var ostream = yield file.create_async (NONE); + yield ostream.write_bytes_async (texture.save_to_png_bytes ()); + + art_url = file.get_uri (); + } catch (Error e) { + critical ("Error saving artwork file: %s", e.message); + } + } + + private string get_art_cache_dir () { + return Path.build_path ( + Path.DIR_SEPARATOR_S, + Environment.get_user_cache_dir (), + GLib.Application.get_default ().application_id, + "art" + ); + } + public static bool equal_func (AudioObject a, AudioObject b) { return (a.uri == b.uri); } diff --git a/src/MetadataDiscoverer.vala b/src/MetadataDiscoverer.vala index 8b8f46a8b..b55357da9 100644 --- a/src/MetadataDiscoverer.vala +++ b/src/MetadataDiscoverer.vala @@ -1,6 +1,11 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + [SingleInstance] public class Music.MetadataDiscoverer : Object { - private Gst.PbUtils.Discoverer discoverer; + private Gst.PbUtils.Discoverer? discoverer; private HashTable objects_to_update; construct { @@ -11,19 +16,21 @@ public class Music.MetadataDiscoverer : Object { } catch (Error e) { critical ("Unable to start Gstreamer Discoverer: %s", e.message); } + objects_to_update = new HashTable (str_hash, str_equal); } - public void request (AudioObject audio) { - objects_to_update.insert (audio.uri, audio); + public void request (AudioObject audio) requires (discoverer != null && !objects_to_update.contains (audio.uri)) { + objects_to_update[audio.uri] = audio; discoverer.start (); discoverer.discover_uri_async (audio.uri); } private void relay_metadata (Gst.PbUtils.DiscovererInfo info, Error? err) { string uri = info.get_uri (); - var audio_obj = objects_to_update.get (uri); - objects_to_update.remove (uri); + + var audio_obj = objects_to_update.take (uri, null); + switch (info.get_result ()) { case Gst.PbUtils.DiscovererResult.URI_INVALID: critical ("Couldn't read metadata for '%s': invalid URI.", uri); diff --git a/src/PlaybackManager.vala b/src/PlaybackManager.vala index 445c6aa13..d9c20c76d 100644 --- a/src/PlaybackManager.vala +++ b/src/PlaybackManager.vala @@ -305,4 +305,81 @@ public class Music.PlaybackManager : Object { next_action.set_enabled (next_sensitive); previous_action.set_enabled (previous_sensitive); } + + private void on_items_changed () { + has_items = queue_liststore.get_n_items () > 0; + shuffle_action.set_enabled (queue_liststore.get_n_items () > 1); + update_next_previous_sensitivity (); + save_queue (); + } + + private void on_audio_changed () { + playbin.set_state (Gst.State.NULL); + if (current_audio != null) { + playbin.uri = current_audio.uri; + playbin.set_state (Gst.State.PLAYING); + } else { + playbin.uri = ""; + playback_position = 0; + + if (progress_timer != 0) { + Source.remove (progress_timer); + progress_timer = 0; + } + } + + update_next_previous_sensitivity (); + + play_pause_action.set_enabled (current_audio != null); + + var uri_last_played = current_audio != null ? current_audio.uri : ""; + settings.set_string ("uri-last-played", uri_last_played); + } + + private void save_queue () { + string[] list_uri = new string[queue_liststore.n_items]; + + for (var i = 0; i < queue_liststore.n_items; i++) { + var item = (Music.AudioObject)queue_liststore.get_item (i); + list_uri[i] = item.uri; + } + + settings.set_strv ("previous-queue", list_uri); + } + + public void restore_queue () { + // Restoring the queue overwrites the last played. So we need to retrieve it before taking care of the queue + var uri_last_played = settings.get_string ("uri-last-played"); + var file_last_played = File.new_for_uri (uri_last_played); + + var last_session_uri = settings.get_strv ("previous-queue"); + if (last_session_uri.length == 0) { + return; + } + + var last_session_files = new File[last_session_uri.length]; + + for (var i = 0; i < last_session_uri.length; i++) { + var uri = last_session_uri[i]; + var file = File.new_for_uri (uri); + last_session_files[i] = file; + } + + var files_to_play = Application.loop_through_files (last_session_files); + queue_files (files_to_play); + + if (uri_last_played != "" && file_last_played.query_exists ()) { + var audio_object = new AudioObject (uri_last_played); + uint position = -1; + if (!queue_liststore.find_with_equal_func ( + audio_object, + (EqualFunc) AudioObject.equal_func, + out position + )) { + return; + } + + current_audio = (AudioObject) queue_liststore.get_item (position); + } + } }