ohmu
·
2026-10-15
1#include "fastfetch.h"
2#include "common/io.h"
3#include "common/strutil.h"
4#include "common/memrchr.h"
5#include "detection/media/media.h"
6
7#include <string.h>
8
9#define FF_DBUS_MPRIS_PREFIX "org.mpris.MediaPlayer2."
10
11#ifdef FF_HAVE_DBUS
12 #include "common/dbus.h"
13
14 #define FF_DBUS_ITER_CONTINUE(dbus, iterator) \
15 { \
16 if (!(dbus)->lib->ffdbus_message_iter_next(iterator)) \
17 break; \
18 continue; \
19 }
20
21static bool parseMprisMetadata(FFDBusData* data, DBusMessageIter* rootIterator, FFMediaResult* result) {
22 DBusMessageIter arrayIterator;
23
24 if (data->lib->ffdbus_message_iter_get_arg_type(rootIterator) == DBUS_TYPE_VARIANT) {
25 DBusMessageIter variantIterator;
26 data->lib->ffdbus_message_iter_recurse(rootIterator, &variantIterator);
27 if (data->lib->ffdbus_message_iter_get_arg_type(&variantIterator) != DBUS_TYPE_ARRAY) {
28 return false;
29 }
30 data->lib->ffdbus_message_iter_recurse(&variantIterator, &arrayIterator);
31 } else {
32 data->lib->ffdbus_message_iter_recurse(rootIterator, &arrayIterator);
33 }
34
35 while (true) {
36 if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) {
37 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
38 }
39
40 DBusMessageIter dictIterator;
41 data->lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator);
42
43 if (data->lib->ffdbus_message_iter_get_arg_type(&dictIterator) != DBUS_TYPE_STRING) {
44 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
45 }
46
47 if (!data->lib->ffdbus_message_iter_has_next(&dictIterator)) {
48 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
49 }
50
51 const char* key;
52 data->lib->ffdbus_message_iter_get_basic(&dictIterator, &key);
53
54 data->lib->ffdbus_message_iter_next(&dictIterator);
55
56 if (ffStrStartsWith(key, "xesam:")) {
57 const char* xesam = key + strlen("xesam:");
58 if (ffStrEquals(xesam, "title")) {
59 ffDBusGetString(data, &dictIterator, &result->song);
60 } else if (ffStrEquals(xesam, "album")) {
61 ffDBusGetString(data, &dictIterator, &result->album);
62 } else if (ffStrEquals(xesam, "artist")) {
63 ffDBusGetString(data, &dictIterator, &result->artist);
64 } else if (ffStrEquals(xesam, "url")) {
65 ffDBusGetString(data, &dictIterator, &result->url);
66 }
67
68 if (result->song.length > 0 && result->artist.length > 0 && result->album.length > 0 && result->url.length > 0) {
69 break;
70 }
71 } else if (ffStrStartsWith(key, "mpris:")) {
72 const char* mpris = key + strlen("mpris:");
73 if (ffStrEquals(mpris, "artUrl")) {
74 FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
75 ffDBusGetString(data, &dictIterator, &path);
76 if (ffStrbufStartsWithS(&path, "file:///")) {
77 ffStrbufEnsureFree(&result->cover, path.length - (uint32_t) strlen("file://"));
78 for (uint32_t i = (uint32_t) strlen("file://"); i < path.length; ++i) {
79 if (path.chars[i] == '%') {
80 if (i + 2 >= path.length) {
81 break;
82 }
83 char str[] = { path.chars[i + 1], path.chars[i + 2], 0 };
84 char* end = nullptr;
85 const char decodedChar = (char) strtoul(str, &end, 16);
86 if (end == &str[2]) {
87 i += 2;
88 ffStrbufAppendC(&result->cover, decodedChar);
89 } else {
90 ffStrbufAppendC(&result->cover, '%');
91 }
92 } else {
93 ffStrbufAppendC(&result->cover, path.chars[i]);
94 }
95 }
96 }
97 } else if (ffStrEquals(mpris, "length")) {
98 int64_t length = 0; // microseconds
99 if (ffDBusGetInt(data, &dictIterator, &length) && length > 0) {
100 result->length = (uint32_t) (length / 1000);
101 }
102 }
103 }
104
105 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
106 }
107
108 return true;
109}
110
111static bool getBusProperties(FFDBusData* data, const char* busName, FFMediaResult* result) {
112 // Get all properties at once to reduce the number of IPCs
113 DBusMessage* reply = ffDBusGetAllProperties(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player");
114 if (reply == nullptr) {
115 return false;
116 }
117
118 DBusMessageIter rootIterator;
119 if (!data->lib->ffdbus_message_iter_init(reply, &rootIterator) &&
120 data->lib->ffdbus_message_iter_get_arg_type(&rootIterator) != DBUS_TYPE_ARRAY) {
121 data->lib->ffdbus_message_unref(reply);
122 return false;
123 }
124
125 DBusMessageIter arrayIterator;
126 data->lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator);
127
128 while (true) {
129 if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) {
130 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
131 }
132
133 DBusMessageIter dictIterator;
134 data->lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator);
135
136 const char* key;
137 data->lib->ffdbus_message_iter_get_basic(&dictIterator, &key);
138
139 data->lib->ffdbus_message_iter_next(&dictIterator);
140
141 if (ffStrEquals(key, "Metadata")) {
142 parseMprisMetadata(data, &dictIterator, result);
143 } else if (ffStrEquals(key, "PlaybackStatus")) {
144 ffDBusGetString(data, &dictIterator, &result->status);
145 } else if (ffStrEquals(key, "Position")) {
146 int64_t position = 0; // microseconds
147 if (ffDBusGetInt(data, &dictIterator, &position) && position > 0) {
148 result->position = (uint32_t) (position / 1000);
149 }
150 }
151
152 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
153 }
154
155 if (result->song.length == 0) {
156 if (result->url.length) {
157 const char* fileName = memrchr(result->url.chars, '/', result->url.length);
158 assert(fileName);
159 ++fileName;
160 ffStrbufEnsureFixedLengthFree(&result->song, result->url.length - (uint32_t) (fileName - result->url.chars));
161 for (; *fileName && *fileName != '?'; ++fileName) {
162 if (*fileName != '%') {
163 ffStrbufAppendC(&result->song, *fileName);
164 } else {
165 if (fileName[1] == 0 || fileName[2] == 0) {
166 break;
167 }
168 char str[] = { fileName[1], fileName[2], 0 };
169 ffStrbufAppendC(&result->song, (char) strtoul(str, nullptr, 16));
170 fileName += 2;
171 }
172 }
173 } else {
174 ffStrbufClear(&result->artist);
175 ffStrbufClear(&result->album);
176 ffStrbufClear(&result->url);
177 ffStrbufClear(&result->status);
178 data->lib->ffdbus_message_unref(reply);
179 return false;
180 }
181 }
182
183 // Set short bus name
184 ffStrbufAppendS(&result->playerId, busName + sizeof(FF_DBUS_MPRIS_PREFIX) - 1);
185
186 // We found a song, get the player name
187 if (ffStrbufStartsWithS(&result->playerId, "musikcube.instance")) {
188 // dbus calls are EXTREMELY slow on musikcube, so we set the player name manually
189 ffStrbufSetStatic(&result->player, "musikcube");
190 } else {
191 ffDBusGetPropertyString(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", "Identity", &result->player);
192 if (result->player.length == 0) {
193 ffDBusGetPropertyString(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", "DesktopEntry", &result->player);
194 }
195 if (result->player.length == 0) {
196 ffStrbufAppend(&result->player, &result->playerId);
197 }
198 }
199
200 data->lib->ffdbus_message_unref(reply);
201
202 return true;
203}
204
205static void getCustomBus(FFDBusData* data, const FFstrbuf* playerName, FFMediaResult* result) {
206 if (ffStrbufStartsWithS(playerName, FF_DBUS_MPRIS_PREFIX)) {
207 getBusProperties(data, playerName->chars, result);
208 return;
209 }
210
211 FF_STRBUF_AUTO_DESTROY busName = ffStrbufCreateS(FF_DBUS_MPRIS_PREFIX);
212 ffStrbufAppend(&busName, playerName);
213 getBusProperties(data, busName.chars, result);
214}
215
216static void getBestBus(FFDBusData* data, FFMediaResult* result) {
217 if (
218 getBusProperties(data, FF_DBUS_MPRIS_PREFIX "spotify", result) ||
219 getBusProperties(data, FF_DBUS_MPRIS_PREFIX "vlc", result) ||
220 getBusProperties(data, FF_DBUS_MPRIS_PREFIX "plasma-browser-integration", result)) {
221 return;
222 }
223
224 DBusMessage* reply = ffDBusGetMethodReply(data, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames", nullptr, nullptr);
225 if (reply == nullptr) {
226 return;
227 }
228
229 DBusMessageIter rootIterator;
230 if (!data->lib->ffdbus_message_iter_init(reply, &rootIterator) || data->lib->ffdbus_message_iter_get_arg_type(&rootIterator) != DBUS_TYPE_ARRAY) {
231 return;
232 }
233
234 DBusMessageIter arrayIterator;
235 data->lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator);
236
237 while (true) {
238 if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_STRING) {
239 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
240 }
241
242 const char* busName;
243 data->lib->ffdbus_message_iter_get_basic(&arrayIterator, &busName);
244
245 if (!ffStrStartsWith(busName, FF_DBUS_MPRIS_PREFIX) ||
246 ffStrEquals(busName + strlen(FF_DBUS_MPRIS_PREFIX), "playerctld")) {
247 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
248 }
249
250 if (getBusProperties(data, busName, result)) {
251 break;
252 }
253
254 FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
255 }
256
257 data->lib->ffdbus_message_unref(reply);
258}
259
260static const char* getMedia(FFMediaResult* result) {
261 FF_DBUS_AUTO_DESTROY_DATA FFDBusData data = {};
262 const char* error = ffDBusLoadData(DBUS_BUS_SESSION, &data);
263 if (error != nullptr) {
264 return error;
265 }
266
267 // FIXME: This is shared for both player and media module.
268 // However it uses an option in one specific module
269 if (instance.config.general.playerName.length > 0) {
270 getCustomBus(&data, &instance.config.general.playerName, result);
271 } else {
272 getBestBus(&data, result);
273 }
274
275 return nullptr;
276}
277
278#endif
279
280// Android compiles this file next to media_android.c, which owns ffDetectMediaImpl and calls this one
281// when the media_session service has nothing to say -- an app UID is refused by that service, while the
282// session bus a desktop environment brings up is open to every process in it. Same arrangement as
283// wallpaper_linux.c and terminalfont_linux.c.
284void
285#ifdef __ANDROID__
286ffDetectMediaLinuxImpl
287#else
288ffDetectMediaImpl
289#endif
290 (FFMediaResult* media, bool saveCover) {
291 FF_UNUSED(saveCover); // We don't save the cover to a file for Mpris implementation
292#ifdef FF_HAVE_DBUS
293 const char* error = getMedia(media);
294 ffStrbufAppendS(&media->error, error);
295#else
296 ffStrbufAppendS(&media->error, "Fastfetch was compiled without DBus support");
297#endif
298}