diff --git a/src/AudioObject.vala b/src/AudioObject.vala index c34046689..75305a07d 100644 --- a/src/AudioObject.vala +++ b/src/AudioObject.vala @@ -5,17 +5,135 @@ 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); } + construct { + title = uri; + 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"); + } + + string art_hash = uri; + if (_artist != null && _album != null) { + art_hash = "%s:%s".printf (_artist, _album); + } + + 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); + } + } + } + } + + 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; + } + + 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 new file mode 100644 index 000000000..b55357da9 --- /dev/null +++ b/src/MetadataDiscoverer.vala @@ -0,0 +1,56 @@ +/* + * 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 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) 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.take (uri, null); + + 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 9376ad3ba..d9c20c76d 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; @@ -52,14 +51,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 (on_items_changed); notify["current-audio"].connect (on_audio_changed); @@ -102,22 +93,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++; @@ -173,101 +152,6 @@ public class Music.PlaybackManager : Object { queue_liststore.remove (position); } - 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 _album; - tag_list.get_string (Gst.Tags.ALBUM, out _album); - if (_album != null) { - audio_object.album = _album; - } - - 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"); - } - - string art_hash = uri; - if (_artist != null && _album != null) { - art_hash = "%s:%s".printf (_artist, _album); - } - - 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 ()) { - audio_object.art_url = art_file.get_uri (); - audio_object.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) { - var texture = Gdk.Texture.for_pixbuf (get_pixbuf_from_buffer (buffer)); - audio_object.texture = texture; - - save_art_file.begin (texture, art_file, (obj, res) => { - try { - audio_object.art_url = save_art_file.end (res); - } catch (Error e) { - critical (e.message); - } - }); - } - } - } - } 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: @@ -422,67 +306,6 @@ 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; - } - - private async string save_art_file (Gdk.Texture texture, File file) throws Error requires (texture != null) { - 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 ()); - - return file.get_uri (); - } - - 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" - ); - } - private void on_items_changed () { has_items = queue_liststore.get_n_items () > 0; shuffle_action.set_enabled (queue_liststore.get_n_items () > 1); diff --git a/src/meson.build b/src/meson.build index 04d907412..eec382c27 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',