1 /*
2 metaprint: display routines for ID3 tags (including filtering of UTF8 to ASCII)
3
4 copyright 2006-2020 by the mpg123 project
5 free software under the terms of the LGPL 2.1
6
7 see COPYING and AUTHORS files in distribution or http://mpg123.org
8 initially written by Thomas Orgis
9 */
10
11 /* Need snprintf(). */
12 #define _DEFAULT_SOURCE
13 #define _BSD_SOURCE
14 // wchar stuff
15 #define _XOPEN_SOURCE 600
16 #define _POSIX_C_SOURCE 200112L
17 #include "common.h"
18 #include "genre.h"
19
20 #include "metaprint.h"
21
22 #include "debug.h"
23
24 int meta_show_lyrics = 0;
25
26 /* Metadata name field texts with index enumeration. */
27 enum tagcode { TITLE=0, ARTIST, ALBUM, COMMENT, YEAR, GENRE, FIELDS };
28 static const char* name[FIELDS] =
29 {
30 "Title"
31 , "Artist"
32 , "Album"
33 , "Comment"
34 , "Year"
35 , "Genre"
36 };
37
38 /* Two-column printing: max length of left and right name strings.
39 see print_id3 for what goes left or right.
40 Choose namelen[0] >= namelen[1]! */
41 static const int namelen[2] = {7, 6};
42 /* Overhead is Name + ": " and also plus " " for right column. */
43 /* pedantic C89 does not like:
44 const int overhead[2] = { namelen[0]+2, namelen[1]+4 }; */
45 static const int overhead[2] = { 9, 10 };
46
mpg_utf8outstr(mpg123_string * dest,mpg123_string * source,int to_terminal)47 static size_t mpg_utf8outstr( mpg123_string *dest, mpg123_string *source
48 , int to_terminal )
49 {
50 size_t ret = utf8outstr( &(dest->p)
51 , (source && source->fill) ? source->p : NULL
52 , to_terminal );
53 dest->size = dest->fill = dest->p ? strlen(dest->p)+1 : 0;
54 return ret;
55 }
56
57 // If the given ID3 string is empty, possibly replace it with ID3v1 data.
id3_gap(mpg123_string * dest,int count,char * v1,size_t * len,int is_term)58 static void id3_gap(mpg123_string *dest, int count, char *v1, size_t *len, int is_term)
59 {
60 if(dest->fill)
61 return;
62 char *utf8tmp = NULL;
63 // First construct some UTF-8 from the id3v1 data, then run through
64 // the same filter as everything else.
65 *len = unknown2utf8(&utf8tmp, v1, count) == 0 ? utf8outstr(&(dest->p), utf8tmp, is_term) : 0;
66 dest->size = dest->fill = dest->p ? strlen(dest->p)+1 : 0;
67 free(utf8tmp);
68 }
69
70 /* Print one metadata entry on a line, aligning the beginning. */
print_oneline(FILE * out,const mpg123_string * tag,enum tagcode fi,int long_mode)71 static void print_oneline( FILE* out
72 , const mpg123_string *tag, enum tagcode fi, int long_mode )
73 {
74 int ret;
75 char fmt[14]; /* "%s:%-XXXs%s\n" plus one null */
76 if(!tag[fi].fill && !long_mode)
77 return;
78
79 if(long_mode)
80 fprintf(out, "\t");
81 ret = snprintf( fmt, sizeof(fmt)-1, "%%s:%%-%ds%%s\n"
82 , 1+namelen[0]-(int)strlen(name[fi]) );
83 if(ret >= sizeof(fmt)-1)
84 fmt[sizeof(fmt)-1] = 0;
85 fprintf(out, fmt, name[fi], " ", tag[fi].fill ? tag[fi].p : "");
86 }
87
88 /*
89 Print a pair of tag name-value pairs along each other in two columns or
90 each on a line if that is not sensible.
91 This takes a given length (in columns) into account, not just bytes.
92 If that length would be computed taking grapheme clusters into account, things
93 could be fine for the whole world of Unicode. So far we ride only on counting
94 possibly multibyte characters (unless mpg123_strlen() got adapted meanwhile).
95 */
print_pair(FILE * out,const int * climit,const mpg123_string * tag,const size_t * len,enum tagcode f0,enum tagcode f1)96 static void print_pair
97 (
98 FILE* out /* Output stream. */
99 , const int *climit /* Maximum width of columns (two values). */
100 , const mpg123_string *tag /* array of tag value strings */
101 , const size_t *len /* array of character/column lengths */
102 , enum tagcode f0, enum tagcode f1 /* field indices for column 0 and 1 */
103 ){
104 /* Two-column printout if things match, dumb printout otherwise. */
105 if( tag[f0].fill && tag[f1].fill
106 && len[f0] <= (size_t)climit[0] && len[f1] <= (size_t)climit[1] )
107 {
108 int ret; // Need to store return value of snprintf to silence gcc.
109 char cfmt[35]; /* "%s:%-XXXs%-XXXs %s:%-XXXs%-XXXs\n" plus one extra null from snprintf */
110 int chardiff[2];
111 size_t bytelen;
112
113 /* difference between character length and byte length */
114 bytelen = strlen(tag[f0].p);
115 chardiff[0] = len[f0] < bytelen ? bytelen-len[f0] : 0;
116 bytelen = strlen(tag[f1].p);
117 chardiff[1] = len[f1] < bytelen ? bytelen-len[f1] : 0;
118
119 /* Two-column format string with added padding for multibyte chars. */
120 ret = snprintf( cfmt, sizeof(cfmt)-1, "%%s:%%-%ds%%-%ds %%s:%%-%ds%%-%ds\n"
121 , 1+namelen[0]-(int)strlen(name[f0]), climit[0]+chardiff[0]
122 , 1+namelen[1]-(int)strlen(name[f1]), climit[1]+chardiff[1] );
123 if(ret >= sizeof(cfmt)-1)
124 cfmt[sizeof(cfmt)-1] = 0;
125 /* Actual printout of name and value pairs. */
126 fprintf(out, cfmt, name[f0], " ", tag[f0].p, name[f1], " ", tag[f1].p);
127 }
128 else
129 {
130 print_oneline(out, tag, f0, FALSE);
131 print_oneline(out, tag, f1, FALSE);
132 }
133 }
134
135 /* Print tags... limiting the UTF-8 to ASCII, if necessary. */
print_id3_tag(mpg123_handle * mh,int long_id3,FILE * out,int linelimit)136 void print_id3_tag(mpg123_handle *mh, int long_id3, FILE *out, int linelimit)
137 {
138 enum tagcode ti;
139 mpg123_string tag[FIELDS];
140 mpg123_string genretmp;
141 size_t len[FIELDS];
142 mpg123_id3v1 *v1;
143 mpg123_id3v2 *v2;
144 int is_term = term_width(fileno(out)) >= 0;
145 if(!is_term)
146 long_id3 = 1;
147 /* no memory allocated here, so return is safe */
148 for(ti=0; ti<FIELDS; ++ti){ len[ti]=0; mpg123_init_string(&tag[ti]); }
149 /* extract the data */
150 mpg123_id3(mh, &v1, &v2);
151 {
152 // Ignore v1 data for Frankenstein streams. It is just somewhere in between.
153 long frank;
154 if(mpg123_getstate(mh, MPG123_FRANKENSTEIN, &frank, NULL) == MPG123_OK && frank)
155 v1 = NULL;
156 }
157
158 /* Only work if something there... */
159 if(v1 == NULL && v2 == NULL) return;
160
161 if(v2 != NULL) /* fill from ID3v2 data */
162 {
163 len[TITLE] = mpg_utf8outstr(&tag[TITLE], v2->title, is_term);
164 len[ARTIST] = mpg_utf8outstr(&tag[ARTIST], v2->artist, is_term);
165 len[ALBUM] = mpg_utf8outstr(&tag[ALBUM], v2->album, is_term);
166 len[COMMENT] = mpg_utf8outstr(&tag[COMMENT], v2->comment, is_term);
167 len[YEAR] = mpg_utf8outstr(&tag[YEAR], v2->year, is_term);
168 }
169 if(v1 != NULL) /* fill gaps with ID3v1 data */
170 {
171 /* I _could_ skip the recalculation of fill ... */
172 id3_gap(&tag[TITLE], 30, v1->title, &len[TITLE], is_term);
173 id3_gap(&tag[ARTIST], 30, v1->artist, &len[ARTIST], is_term);
174 id3_gap(&tag[ALBUM], 30, v1->album, &len[ALBUM], is_term);
175 id3_gap(&tag[COMMENT], 30, v1->comment, &len[COMMENT], is_term);
176 id3_gap(&tag[YEAR], 4, v1->year, &len[YEAR], is_term);
177 }
178 // Genre is special... v1->genre holds an index, id3v2 genre may contain
179 // indices in textual form and raw textual genres...
180 mpg123_init_string(&genretmp);
181 if(v2 && v2->genre && v2->genre->fill)
182 {
183 /*
184 id3v2.3 says (id)(id)blabla and in case you want ot have (blabla) write ((blabla)
185 also, there is
186 (RX) Remix
187 (CR) Cover
188 id3v2.4 says
189 "one or several of the ID3v1 types as numerical strings"
190 or define your own (write strings), RX and CR
191
192 Now I am very sure that I'll encounter hellishly mixed up id3v2 frames, so try to parse both at once.
193 */
194 size_t num = 0;
195 size_t nonum = 0;
196 size_t i;
197 enum { nothing, number, outtahere } state = nothing;
198 /* number\n -> id3v1 genre */
199 /* (number) -> id3v1 genre */
200 /* (( -> ( */
201 debug1("interpreting genre: %s\n", v2->genre->p);
202 for(i = 0; i < v2->genre->fill; ++i)
203 {
204 debug1("i=%lu", (unsigned long) i);
205 switch(state)
206 {
207 case nothing:
208 nonum = i;
209 if(v2->genre->p[i] == '(')
210 {
211 num = i+1; /* number starting as next? */
212 state = number;
213 debug1("( before number at %lu?", (unsigned long) num);
214 }
215 else if(v2->genre->p[i] >= '0' && v2->genre->p[i] <= '9')
216 {
217 num = i;
218 state = number;
219 debug1("direct number at %lu", (unsigned long) num);
220 }
221 else state = outtahere;
222 break;
223 case number:
224 /* fake number alert: (( -> ( */
225 if(v2->genre->p[i] == '(')
226 {
227 nonum = i;
228 state = outtahere;
229 debug("no, it was ((");
230 }
231 else if(v2->genre->p[i] == ')' || v2->genre->p[i] == '\n' || v2->genre->p[i] == 0)
232 {
233 if(i-num > 0)
234 {
235 /* we really have a number */
236 int gid;
237 char* genre = "Unknown";
238 v2->genre->p[i] = 0;
239 gid = atoi(v2->genre->p+num);
240
241 /* get that genre */
242 if(gid >= 0 && gid <= genre_count) genre = genre_table[gid];
243 debug1("found genre: %s", genre);
244
245 if(genretmp.fill) mpg123_add_string(&genretmp, ", ");
246 mpg123_add_string(&genretmp, genre);
247 nonum = i+1; /* next possible stuff */
248 state = nothing;
249 debug1("had a number: %i", gid);
250 }
251 else
252 {
253 /* wasn't a number, nonum is set */
254 state = outtahere;
255 debug("no (num) thing...");
256 }
257 }
258 else if(!(v2->genre->p[i] >= '0' && v2->genre->p[i] <= '9'))
259 {
260 /* no number at last... */
261 state = outtahere;
262 debug("nothing numeric here");
263 }
264 else
265 {
266 debug("still number...");
267 }
268 break;
269 default: break;
270 }
271 if(state == outtahere) break;
272 }
273 /* Small hack: Avoid repeating genre in case of stuff like
274 (144)Thrash Metal being given. The simple cases. */
275 if(
276 nonum < v2->genre->fill-1 &&
277 (!genretmp.fill || strncmp(genretmp.p, v2->genre->p+nonum, genretmp.fill))
278 )
279 {
280 if(genretmp.fill) mpg123_add_string(&genretmp, ", ");
281 mpg123_add_string(&genretmp, v2->genre->p+nonum);
282 }
283 }
284 else if(v1)
285 {
286 // Fill from v1 tag.
287 if(mpg123_resize_string(&genretmp, 31))
288 {
289 if(v1->genre <= genre_count)
290 strncpy(genretmp.p, genre_table[v1->genre], 30);
291 else
292 strncpy(genretmp.p,"Unknown",30);
293 genretmp.p[30] = 0;
294 genretmp.fill = strlen(genretmp.p)+1;
295 }
296 }
297 // Finally convert to safe output string and get display width.
298 len[GENRE] = mpg_utf8outstr(&tag[GENRE], &genretmp, is_term);
299 mpg123_free_string(&genretmp);
300
301 if(long_id3)
302 {
303 fprintf(out,"\n");
304 /* print id3v2 */
305 print_oneline(out, tag, TITLE, TRUE);
306 print_oneline(out, tag, ARTIST, TRUE);
307 print_oneline(out, tag, ALBUM, TRUE);
308 print_oneline(out, tag, YEAR, TRUE);
309 print_oneline(out, tag, GENRE, TRUE);
310 print_oneline(out, tag, COMMENT, TRUE);
311 fprintf(out,"\n");
312 }
313 else
314 {
315 /* We are trying to be smart here and conserve some vertical space.
316 So we will skip tags not set, and try to show them in two parallel
317 columns if they are short, which is by far the most common case. */
318 int climit[2];
319
320 /* Adapt formatting width to terminal if possible. */
321 if(linelimit < 0)
322 linelimit=overhead[0]+30+overhead[1]+30; /* the old style, based on ID3v1 */
323 if(linelimit > 200)
324 linelimit = 200; /* Not too wide. Also for format string safety. */
325 /* Divide the space between the two columns, not wasting any. */
326 climit[1] = linelimit/2-overhead[0];
327 climit[0] = linelimit-linelimit/2-overhead[1];
328 debug3("linelimits: %i < %i | %i >", linelimit, climit[0], climit[1]);
329
330 if(climit[0] <= 0 || climit[1] <= 0)
331 {
332 /* Ensure disabled column printing, no play with signedness in comparisons. */
333 climit[0] = 0;
334 climit[1] = 0;
335 }
336 fprintf(out,"\n"); /* Still use one separator line. Too ugly without. */
337 print_pair(out, climit, tag, len, TITLE, ARTIST);
338 print_pair(out, climit, tag, len, COMMENT, ALBUM );
339 print_pair(out, climit, tag, len, YEAR, GENRE );
340 }
341 for(ti=0; ti<FIELDS; ++ti) mpg123_free_string(&tag[ti]);
342
343 if(v2 != NULL && meta_show_lyrics)
344 {
345 /* find and print texts that have USLT IDs */
346 size_t i;
347 for(i=0; i<v2->texts; ++i)
348 {
349 if(!memcmp(v2->text[i].id, "USLT", 4))
350 {
351 /* split into lines, ensure usage of proper local line end */
352 size_t a=0;
353 size_t b=0;
354 char lang[4]; /* just a 3-letter ASCII code, no fancy encoding */
355 mpg123_string innline;
356 mpg123_string outline;
357 mpg123_string *uslt = &v2->text[i].text;
358
359 memcpy(lang, &v2->text[i].lang, 3);
360 lang[3] = 0;
361 printf("Lyrics begin, language: %s; %s\n\n", lang, v2->text[i].description.fill ? v2->text[i].description.p : "");
362
363 mpg123_init_string(&innline);
364 mpg123_init_string(&outline);
365 while(a < uslt->fill)
366 {
367 b = a;
368 while(b < uslt->fill && uslt->p[b] != '\n' && uslt->p[b] != '\r') ++b;
369 /* Either found end of a line or end of the string (null byte) */
370 mpg123_set_substring(&innline, uslt->p, a, b-a);
371 mpg_utf8outstr(&outline, &innline, is_term);
372 printf(" %s\n", outline.p);
373
374 if(uslt->p[b] == uslt->fill) break; /* nothing more */
375
376 /* Swallow CRLF */
377 if(uslt->fill-b > 1 && uslt->p[b] == '\r' && uslt->p[b+1] == '\n') ++b;
378
379 a = b + 1; /* next line beginning */
380 }
381 mpg123_free_string(&innline);
382 mpg123_free_string(&outline);
383
384 printf("\nLyrics end.\n");
385 }
386 }
387 }
388 }
389
print_icy(mpg123_handle * mh,FILE * outstream)390 void print_icy(mpg123_handle *mh, FILE *outstream)
391 {
392 int is_term = term_width(fileno(outstream)) >= 0;
393 char* icy;
394 if(MPG123_OK == mpg123_icy(mh, &icy))
395 {
396 mpg123_string in;
397 mpg123_init_string(&in);
398 if(mpg123_store_utf8(&in, mpg123_text_icy, (unsigned char*)icy, strlen(icy)+1))
399 {
400 char *out = NULL;
401 utf8outstr(&out, in.p, is_term);
402 if(out)
403 fprintf(outstream, "\nICY-META: %s\n", out);
404
405 free(out);
406 }
407 mpg123_free_string(&in);
408 }
409 }
410