master ohmu/fastfetch-openbsd / src / modules / media / media.c
ohmu  ·  2026-10-15
  1#include "common/printing.h"
  2#include "common/jsonconfig.h"
  3#include "common/percent.h"
  4#include "detection/media/media.h"
  5#include "modules/media/media.h"
  6
  7#include <ctype.h>
  8
  9static inline bool shouldIgnoreChar(char c) {
 10    return isblank(c) || c == '-' || c == '.';
 11}
 12
 13static bool artistInSongTitle(const FFstrbuf* song, const FFstrbuf* artist) {
 14    uint32_t artistIndex = 0;
 15    uint32_t songIndex = 0;
 16
 17    while (true) {
 18        while (shouldIgnoreChar(song->chars[songIndex])) {
 19            ++songIndex;
 20        }
 21
 22        while (shouldIgnoreChar(artist->chars[artistIndex])) {
 23            ++artistIndex;
 24        }
 25
 26        if (artist->chars[artistIndex] == '\0') {
 27            return true;
 28        }
 29
 30        if (song->chars[songIndex] == '\0') {
 31            return false;
 32        }
 33
 34        if (tolower(song->chars[songIndex]) != tolower(artist->chars[artistIndex])) {
 35            return false;
 36        }
 37
 38        ++artistIndex;
 39        ++songIndex;
 40    }
 41
 42    // Unreachable
 43    return false;
 44}
 45
 46static void appendProgress(FFstrbuf* buffer, const FFMediaResult* media) {
 47    uint32_t sLen = media->length / 1000, sPos = media->position / 1000;
 48    if (sLen > 60 * 60) {
 49        ffStrbufAppendF(buffer, "%02u:%02u:%02u / %02u:%02u:%02u", sPos / 3600, (sPos % 3600) / 60, sPos % 60, sLen / 3600, (sLen % 3600) / 60, sLen % 60);
 50    } else {
 51        ffStrbufAppendF(buffer, "%02u:%02u / %02u:%02u", sPos / 60, sPos % 60, sLen / 60, sLen % 60);
 52    }
 53}
 54
 55bool ffPrintMedia(FFMediaOptions* options) {
 56    const FFMediaResult* media = ffDetectMedia(false);
 57
 58    if (media->error.length > 0) {
 59        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Media), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", media->error.chars);
 60        return false;
 61    }
 62
 63    FF_STRBUF_AUTO_DESTROY songPretty = ffStrbufCreateCopy(&media->song);
 64    const char* removeStrings[] = {
 65        "(Official Music Video)",
 66        "(Official Video)",
 67        "(Music Video)",
 68        "(Official HD Video)",
 69        "[Official Music Video]",
 70        "[Official Video]",
 71        "[Music Video]",
 72        "[Official HD Video]",
 73        "| Official Music Video",
 74        "| Official Video",
 75        "| Music Video",
 76        "| Official HD Video",
 77        "[Official Audio]",
 78        "[Audio]",
 79        "(Audio)",
 80        "| Official Audio",
 81        "| Audio",
 82        "| OFFICIAL AUDIO",
 83        "(Lyric Video)",
 84        "(Official Lyric Video)",
 85        "(Lyrics)",
 86        "[Lyric Video]",
 87        "[Official Lyric Video]",
 88        "[Lyrics]",
 89        "| Lyric Video",
 90        "| Official Lyric Video",
 91        "| Lyrics",
 92    };
 93    ffStrbufRemoveStrings(&songPretty, ARRAY_SIZE(removeStrings), removeStrings);
 94    ffStrbufTrimRight(&songPretty, ' ');
 95
 96    if (songPretty.length == 0) {
 97        ffStrbufAppend(&songPretty, &media->song);
 98    }
 99
100    FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
101
102    if (options->moduleArgs.outputFormat.length == 0) {
103        // We don't expose artistPretty to the format, as it might be empty (when the think that the artist is already in the song title)
104        FF_STRBUF_AUTO_DESTROY artistPretty = ffStrbufCreateCopy(&media->artist);
105        ffStrbufRemoveIgnCaseEndS(&artistPretty, " - Topic");
106        ffStrbufRemoveIgnCaseEndS(&artistPretty, "VEVO");
107        ffStrbufTrimRight(&artistPretty, ' ');
108
109        if (artistInSongTitle(&songPretty, &artistPretty)) {
110            ffStrbufClear(&artistPretty);
111        }
112
113        ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Media), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
114
115        if (artistPretty.length > 0) {
116            ffStrbufWriteTo(&artistPretty, stdout);
117            fputs(" - ", stdout);
118        }
119
120        if (media->length > 0) {
121            if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
122                ffStrbufAppendS(&songPretty, " - ");
123                appendProgress(&songPretty, media);
124            }
125
126            if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
127                ffStrbufAppendC(&songPretty, ' ');
128                ffPercentAppendNum(
129                    &songPretty,
130                    media->position * 100.0 / media->length,
131                    options->percent,
132                    true,
133                    &options->moduleArgs);
134            }
135            if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
136                ffStrbufAppendC(&songPretty, ' ');
137                ffPercentAppendBar(
138                    &songPretty,
139                    media->position * 100.0 / media->length,
140                    options->percent,
141                    &options->moduleArgs);
142            }
143        }
144
145        if (media->status.length > 0) {
146            ffStrbufAppendF(&songPretty, " [%s]", media->status.chars);
147        }
148
149        ffStrbufPutTo(&songPretty, stdout);
150    } else {
151        FF_STRBUF_AUTO_DESTROY progress = ffStrbufCreate();
152        FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
153        FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
154        if (media->length > 0) {
155            if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
156                appendProgress(&progress, media);
157            }
158            if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
159                ffPercentAppendNum(
160                    &percentageNum,
161                    media->position * 100.0 / media->length,
162                    options->percent,
163                    false,
164                    &options->moduleArgs);
165            }
166            if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
167                ffPercentAppendBar(
168                    &percentageBar,
169                    media->position * 100.0 / media->length,
170                    options->percent,
171                    &options->moduleArgs);
172            }
173        }
174        FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Media), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]){
175                                                                                                          FF_ARG(songPretty, "combined"),
176                                                                                                          FF_ARG(media->song, "title"),
177                                                                                                          FF_ARG(media->artist, "artist"),
178                                                                                                          FF_ARG(media->album, "album"),
179                                                                                                          FF_ARG(media->status, "status"),
180                                                                                                          FF_ARG(progress, "progress"),
181                                                                                                          FF_ARG(percentageNum, "progress-num"),
182                                                                                                          FF_ARG(percentageBar, "progress-bar"),
183                                                                                                          FF_ARG(media->player, "player-name"),
184                                                                                                          FF_ARG(media->playerId, "player-id"),
185                                                                                                          FF_ARG(media->url, "url"),
186                                                                                                      }));
187    }
188
189    return true;
190}
191
192void ffParseMediaJsonObject(FFMediaOptions* options, yyjson_val* module) {
193    yyjson_val *key, *val;
194    size_t idx, max;
195    yyjson_obj_foreach (module, idx, max, key, val) {
196        if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
197            continue;
198        }
199
200        if (ffPercentParseJsonObject(key, val, &options->percent)) {
201            continue;
202        }
203
204        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Media), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
205    }
206}
207
208[[gnu::cold]]
209void ffGenerateMediaJsonConfig(FFMediaOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
210    ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
211
212    ffPercentGenerateJsonConfig(doc, module, options->percent);
213}
214
215bool ffGenerateMediaJsonResult([[maybe_unused]] FFMediaOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
216    const FFMediaResult* media = ffDetectMedia(false);
217
218    if (media->error.length > 0) {
219        yyjson_mut_obj_add_strbuf(doc, module, "error", &media->error);
220        return false;
221    }
222
223    yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
224
225    yyjson_mut_val* song = yyjson_mut_obj_add_obj(doc, obj, "song");
226    yyjson_mut_obj_add_strbuf(doc, song, "name", &media->song);
227    yyjson_mut_obj_add_strbuf(doc, song, "artist", &media->artist);
228    yyjson_mut_obj_add_strbuf(doc, song, "album", &media->album);
229    yyjson_mut_obj_add_strbuf(doc, song, "status", &media->status);
230    yyjson_mut_obj_add_uint(doc, song, "length", media->length);
231    yyjson_mut_obj_add_uint(doc, song, "position", media->position);
232    if (media->cover.length > 0) {
233        yyjson_mut_obj_add_strbuf(doc, song, "cover", &media->cover);
234    } else {
235        yyjson_mut_obj_add_null(doc, song, "cover");
236    }
237
238    yyjson_mut_val* player = yyjson_mut_obj_add_obj(doc, obj, "player");
239    yyjson_mut_obj_add_strbuf(doc, player, "name", &media->player);
240    yyjson_mut_obj_add_strbuf(doc, player, "id", &media->playerId);
241    yyjson_mut_obj_add_strbuf(doc, player, "url", &media->url);
242
243    return true;
244}
245
246void ffInitMediaOptions(FFMediaOptions* options) {
247    ffOptionInitModuleArg(&options->moduleArgs, "");
248
249    options->percent = (FFPercentageModuleConfig){ 100, 100, 0 };
250}
251
252void ffDestroyMediaOptions(FFMediaOptions* options) {
253    ffOptionDestroyModuleArg(&options->moduleArgs);
254}
255
256FFModuleBaseInfo ffMediaModuleInfo = {
257    .name = "Media",
258    .description = "Print the name of the currently playing song",
259    .displayName = {
260        .en = "Media",
261        .ar = "الوسائط",
262        .cs = "Média",
263        .de = "Medien",
264        .es = "Multimedia",
265        .fi = "Multimedia",
266        .fr = "Médias",
267        .gl = "Multimedia",
268        .he = "מדיה",
269        .id = "Media",
270        .it = "Media",
271        .ja = "メディア",
272        .ko = "미디어",
273        .pl = "Media",
274        .pt = "Mídia",
275        .ru = "Медиа",
276        .tr = "Medya",
277        .uk = "Медіа",
278        .vi = "Phương tiện",
279        .zh_CN = "媒体",
280        .zh_TW = "媒體",
281    },
282    .initOptions = (void*) ffInitMediaOptions,
283    .destroyOptions = (void*) ffDestroyMediaOptions,
284    .parseJsonObject = (void*) ffParseMediaJsonObject,
285    .printModule = (void*) ffPrintMedia,
286    .generateJsonResult = (void*) ffGenerateMediaJsonResult,
287    .generateJsonConfig = (void*) ffGenerateMediaJsonConfig,
288    .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){
289        { "Pretty media name", "combined" },
290        { "Media name", "title" },
291        { "Artist name", "artist" },
292        { "Album name", "album" },
293        { "Status", "status" },
294        { "Progress in text", "progress" },
295        { "Progress in percentage (number)", "progress-num" },
296        { "Progress in percentage (bar)", "progress-bar" },
297        { "Player name", "player-name" },
298        { "Player ID", "player-id" },
299        { "URL", "url" },
300    })),
301    .defaultOrder = 48,
302};