Skip to content

Commit 3eebdd2

Browse files
leolost2605Oowoosh0danirabbitzeebok
authored
Metadata in AudioObject (#834)
* Handle metadata discovery at creation of AudioObjects * Fix lint error * Fix merge --------- Co-authored-by: oowoosh0 <mail@oowoo.sh> Co-authored-by: Danielle Foré <danielle@elementary.io> Co-authored-by: Ryan Kornheisl <ryan@skarva.tech>
1 parent 07dd007 commit 3eebdd2

4 files changed

Lines changed: 181 additions & 183 deletions

File tree

src/AudioObject.vala

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,135 @@
55

66
public class Music.AudioObject : Object {
77
public string uri { get; construct; }
8-
public Gdk.Texture? texture { get; set; default = null; }
9-
public string album { get; set; }
10-
public string artist { get; set; }
11-
public string title { get; set; }
12-
public int64 duration { get; set; default = 0; }
13-
public string art_url { get; set; default = ""; }
8+
public Gdk.Texture? texture { get; private set; default = null; }
9+
public string album { get; private set; }
10+
public string artist { get; private set; }
11+
public string title { get; private set; }
12+
public int64 duration { get; private set; default = 0; }
13+
public string art_url { get; private set; default = ""; }
14+
15+
private static MetadataDiscoverer discoverer = new MetadataDiscoverer ();
1416

1517
public AudioObject (string uri) {
1618
Object (uri: uri);
1719
}
1820

21+
construct {
22+
title = uri;
23+
discoverer.request (this);
24+
}
25+
26+
public void update_metadata (Gst.PbUtils.DiscovererInfo info) {
27+
duration = (int64) info.get_duration ();
28+
29+
unowned Gst.TagList? tag_list = info.get_tags ();
30+
31+
string _title;
32+
tag_list.get_string (Gst.Tags.TITLE, out _title);
33+
if (_title != null) {
34+
title = _title;
35+
}
36+
37+
string _artist;
38+
tag_list.get_string (Gst.Tags.ARTIST, out _artist);
39+
if (_artist != null) {
40+
artist = _artist;
41+
} else if (_title != null) { // Don't set artist for files without tags
42+
artist = _("Unknown");
43+
}
44+
45+
string art_hash = uri;
46+
if (_artist != null && _album != null) {
47+
art_hash = "%s:%s".printf (_artist, _album);
48+
}
49+
50+
var art_file = File.new_for_path (Path.build_path (
51+
Path.DIR_SEPARATOR_S,
52+
get_art_cache_dir (),
53+
Checksum.compute_for_string (SHA256, art_hash)
54+
));
55+
56+
if (art_file.query_exists ()) {
57+
art_url = art_file.get_uri ();
58+
texture = Gdk.Texture.from_file (art_file);
59+
} else {
60+
var sample = get_cover_sample (tag_list);
61+
if (sample != null) {
62+
var buffer = sample.get_buffer ();
63+
64+
if (buffer != null) {
65+
texture = Gdk.Texture.for_pixbuf (get_pixbuf_from_buffer (buffer));
66+
save_art_file.begin (texture, art_file);
67+
}
68+
}
69+
}
70+
}
71+
72+
private Gst.Sample? get_cover_sample (Gst.TagList tag_list) {
73+
Gst.Sample cover_sample = null;
74+
Gst.Sample sample;
75+
for (int i = 0; tag_list.get_sample_index (Gst.Tags.IMAGE, i, out sample); i++) {
76+
var caps = sample.get_caps ();
77+
unowned Gst.Structure caps_struct = caps.get_structure (0);
78+
int image_type = Gst.Tag.ImageType.UNDEFINED;
79+
caps_struct.get_enum ("image-type", typeof (Gst.Tag.ImageType), out image_type);
80+
if (image_type == Gst.Tag.ImageType.UNDEFINED && cover_sample == null) {
81+
cover_sample = sample;
82+
} else if (image_type == Gst.Tag.ImageType.FRONT_COVER) {
83+
return sample;
84+
}
85+
}
86+
87+
return cover_sample;
88+
}
89+
90+
private Gdk.Pixbuf? get_pixbuf_from_buffer (Gst.Buffer buffer) {
91+
Gst.MapInfo map_info;
92+
93+
if (!buffer.map (out map_info, Gst.MapFlags.READ)) {
94+
warning ("Could not map memory buffer");
95+
return null;
96+
}
97+
98+
Gdk.Pixbuf pix = null;
99+
100+
try {
101+
var loader = new Gdk.PixbufLoader ();
102+
103+
if (loader.write (map_info.data) && loader.close ()) {
104+
pix = loader.get_pixbuf ();
105+
}
106+
} catch (Error err) {
107+
warning ("Error processing image data: %s", err.message);
108+
}
109+
110+
buffer.unmap (map_info);
111+
112+
return pix;
113+
}
114+
115+
private async void save_art_file (Gdk.Texture? texture, File file) requires (texture != null) {
116+
try {
117+
DirUtils.create_with_parents (get_art_cache_dir (), 0755);
118+
119+
var ostream = yield file.create_async (NONE);
120+
yield ostream.write_bytes_async (texture.save_to_png_bytes ());
121+
122+
art_url = file.get_uri ();
123+
} catch (Error e) {
124+
critical ("Error saving artwork file: %s", e.message);
125+
}
126+
}
127+
128+
private string get_art_cache_dir () {
129+
return Path.build_path (
130+
Path.DIR_SEPARATOR_S,
131+
Environment.get_user_cache_dir (),
132+
GLib.Application.get_default ().application_id,
133+
"art"
134+
);
135+
}
136+
19137
public static bool equal_func (AudioObject a, AudioObject b) {
20138
return (a.uri == b.uri);
21139
}

src/MetadataDiscoverer.vala

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-3.0-or-later
3+
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
4+
*/
5+
6+
[SingleInstance]
7+
public class Music.MetadataDiscoverer : Object {
8+
private Gst.PbUtils.Discoverer? discoverer;
9+
private HashTable<string, AudioObject> objects_to_update;
10+
11+
construct {
12+
try {
13+
discoverer = new Gst.PbUtils.Discoverer ((Gst.ClockTime) (5 * Gst.SECOND));
14+
discoverer.discovered.connect (relay_metadata);
15+
discoverer.finished.connect (discoverer.stop);
16+
} catch (Error e) {
17+
critical ("Unable to start Gstreamer Discoverer: %s", e.message);
18+
}
19+
20+
objects_to_update = new HashTable<string, AudioObject> (str_hash, str_equal);
21+
}
22+
23+
public void request (AudioObject audio) requires (discoverer != null && !objects_to_update.contains (audio.uri)) {
24+
objects_to_update[audio.uri] = audio;
25+
discoverer.start ();
26+
discoverer.discover_uri_async (audio.uri);
27+
}
28+
29+
private void relay_metadata (Gst.PbUtils.DiscovererInfo info, Error? err) {
30+
string uri = info.get_uri ();
31+
32+
var audio_obj = objects_to_update.take (uri, null);
33+
34+
switch (info.get_result ()) {
35+
case Gst.PbUtils.DiscovererResult.URI_INVALID:
36+
critical ("Couldn't read metadata for '%s': invalid URI.", uri);
37+
return;
38+
case Gst.PbUtils.DiscovererResult.ERROR:
39+
critical ("Couldn't read metadata for '%s': %s", uri, err.message);
40+
return;
41+
case Gst.PbUtils.DiscovererResult.TIMEOUT:
42+
critical ("Couldn't read metadata for '%s': Discovery timed out.", uri);
43+
return;
44+
case Gst.PbUtils.DiscovererResult.BUSY:
45+
critical ("Couldn't read metadata for '%s': Already discovering a file.", uri);
46+
return;
47+
case Gst.PbUtils.DiscovererResult.MISSING_PLUGINS:
48+
critical ("Couldn't read metadata for '%s': Missing plugins.", uri);
49+
return;
50+
default:
51+
break;
52+
}
53+
54+
audio_obj.update_metadata (info);
55+
}
56+
}

0 commit comments

Comments
 (0)