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};