master ohmu/fastfetch-openbsd / src / detection / media / media_linux.c
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}