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