Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 124 additions & 6 deletions src/AudioObject.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
56 changes: 56 additions & 0 deletions src/MetadataDiscoverer.vala
Original file line number Diff line number Diff line change
@@ -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<string, AudioObject> 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<string, AudioObject> (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);
}
}
Loading
Loading