1 /*
2 * Copyright (c) 2010, 2011 Ryan Flannery <ryan.flannery@gmail.com>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include "meta_info.h"
18
19 /* human-readable names of all of the string-type meta information values */
20 const char *MI_CINFO_NAMES[] = {
21 "Artist",
22 "Album",
23 "Title",
24 "Track",
25 "Year",
26 "Genre",
27 "Length",
28 "Comment"
29 };
30
31 /*
32 * Create and return a new meta_info struct. All memory is allocated, and
33 * the resulting pointer should be free(3)'d using mi_free().
34 */
35 meta_info *
mi_new(void)36 mi_new(void)
37 {
38 meta_info *mi;
39 int i;
40
41 if ((mi = malloc(sizeof(meta_info))) == NULL)
42 err(1, "mi_new: meta_info malloc failed");
43
44 mi->filename = NULL;
45 mi->length = 0;
46 mi->last_updated = 0;
47 mi->is_url = false;
48
49 for (i = 0; i < MI_NUM_CINFO; i++)
50 mi->cinfo[i] = NULL;
51
52 return mi;
53 }
54
55
56 /* Function to free() all memory allocated by a given meta_info struct. */
57 void
mi_free(meta_info * mi)58 mi_free(meta_info *mi)
59 {
60 int i;
61
62 if (mi->filename != NULL)
63 free(mi->filename);
64
65 for (i = 0; i < MI_NUM_CINFO; i++) {
66 if (mi->cinfo[i] != NULL)
67 free(mi->cinfo[i]);
68 }
69
70 free(mi);
71 }
72
73 /* Function to write a meta_info struct to a file stream. */
74 void
mi_fwrite(meta_info * mi,FILE * fout)75 mi_fwrite(meta_info *mi, FILE *fout)
76 {
77 static uint16_t lengths[MI_NUM_CINFO + 1]; /* +1 for filename */
78 int i;
79
80 /* store all necessary numeric values in the above array */
81 lengths[0] = strlen(mi->filename);
82 for (i = 0; i < MI_NUM_CINFO; i++) {
83 if (mi->cinfo[i] == NULL)
84 lengths[i+1] = 0;
85 else
86 lengths[i+1] = strlen(mi->cinfo[i]);
87 }
88
89 /* write */
90 fwrite(lengths, sizeof(lengths), 1, fout);
91 fwrite(mi->filename, sizeof(char), lengths[0], fout);
92 for (i = 0; i < MI_NUM_CINFO; i++) {
93 if (mi->cinfo[i] != NULL)
94 fwrite(mi->cinfo[i], sizeof(char), lengths[i+1], fout);
95 }
96 fwrite(&(mi->length), sizeof(int), 1, fout);
97 fwrite(&(mi->last_updated), sizeof(time_t), 1, fout);
98 fwrite(&(mi->is_url), sizeof(bool), 1, fout);
99
100 fflush(fout);
101 }
102
103 /* Function to read a meta_info struct from a file stream */
104 void
mi_fread(meta_info * mi,FILE * fin)105 mi_fread(meta_info *mi, FILE *fin)
106 {
107 static uint16_t lengths[MI_NUM_CINFO + 1]; /* +1 for filename */
108 int i;
109
110 /* first read all necessary numeric values */
111 fread(lengths, sizeof(lengths), 1, fin);
112
113 /* allocate all needed space in the meta_info struct, and zero */
114 if ((mi->filename = calloc(lengths[0] + 1, sizeof(char))) == NULL)
115 err(1, "mi_fread: calloc filename failed");
116
117 bzero(mi->filename, sizeof(char) * (lengths[0] + 1));
118
119 for (i = 0; i < MI_NUM_CINFO; i++) {
120 if (lengths[i+1] > 0) {
121 if ((mi->cinfo[i] = calloc(lengths[i+1] + 1, sizeof(char))) == NULL)
122 err(1, "mi_fread: failed to calloc cinfo");
123
124 bzero(mi->cinfo[i], sizeof(char) * (lengths[i+1] + 1));
125 }
126 }
127
128 /* read */
129 fread(mi->filename, sizeof(char), lengths[0], fin);
130 for (i = 0; i < MI_NUM_CINFO; i++) {
131 if (lengths[i+1] > 0)
132 fread(mi->cinfo[i], sizeof(char), lengths[i+1], fin);
133 }
134 fread(&(mi->length), sizeof(int), 1, fin);
135 fread(&(mi->last_updated), sizeof(time_t), 1, fin);
136 fread(&(mi->is_url), sizeof(bool), 1, fin);
137 }
138
139 /* given a number of seconds s, format a "hh:mm::ss" string */
140 char *
time2str(int s)141 time2str(int s)
142 {
143 static char str[255];
144 int hours, minutes, seconds;
145
146 if (s <= 0)
147 return "?";
148
149 hours = s / 3600;
150 minutes = s % 3600 / 60;
151 seconds = s % 60;
152
153 if (hours > 0)
154 snprintf(str, sizeof(str), "%i:%02i:%02i", hours, minutes, seconds);
155 else if (minutes > 0)
156 snprintf(str, sizeof(str), "%i:%02i", minutes, seconds);
157 else
158 snprintf(str, sizeof(str), "%is", seconds);
159
160 return str;
161 }
162
163
164 /*
165 * Extract meta-information from the provided file, returning a new
166 * meta_info* struct if any information was found, NULL if no information
167 * was available.
168 */
169 meta_info *
mi_extract(const char * filename)170 mi_extract(const char *filename)
171 {
172 static char fullname[PATH_MAX];
173 const TagLib_AudioProperties *properties;
174 TagLib_File *file;
175 TagLib_Tag *tag;
176 char *str;
177
178 /* create new, empty meta info struct */
179 meta_info *mi = mi_new();
180
181 /* store full filename in meta_info struct */
182 bzero(fullname, sizeof(fullname));
183 if (realpath(filename, fullname) == NULL)
184 err(1, "mi_extract: realpath failed to resolve '%s'", filename);
185
186 if ((mi->filename = strdup(fullname)) == NULL)
187 errx(1, "mi_extract: strdup failed for '%s'", fullname);
188
189 /* start extracting fields using TagLib... */
190
191 taglib_set_strings_unicode(false);
192
193 if ((file = taglib_file_new(mi->filename)) == NULL
194 || !taglib_file_is_valid(file)) {
195 mi_free(mi);
196 return NULL;
197 }
198
199 /* extract tag-info + audio properties (length) */
200 tag = taglib_file_tag(file);
201 properties = taglib_file_audioproperties(file);
202
203 /* artist/album/title/genre */
204 if ((str = taglib_tag_artist(tag)) != NULL)
205 mi->cinfo[MI_CINFO_ARTIST] = strdup(str);
206
207 if ((str = taglib_tag_album(tag)) != NULL)
208 mi->cinfo[MI_CINFO_ALBUM] = strdup(str);
209
210 if ((str = taglib_tag_title(tag)) != NULL)
211 mi->cinfo[MI_CINFO_TITLE] = strdup(str);
212
213 if ((str = taglib_tag_genre(tag)) != NULL)
214 mi->cinfo[MI_CINFO_GENRE] = strdup(str);
215
216 if ((str = taglib_tag_comment(tag)) != NULL)
217 mi->cinfo[MI_CINFO_COMMENT] = strdup(str);
218
219 if (mi->cinfo[MI_CINFO_ARTIST] == NULL
220 || mi->cinfo[MI_CINFO_ALBUM] == NULL
221 || mi->cinfo[MI_CINFO_TITLE] == NULL
222 || mi->cinfo[MI_CINFO_GENRE] == NULL
223 || mi->cinfo[MI_CINFO_COMMENT] == NULL)
224 err(1, "mi_extract: strdup for CINFO failed");
225
226 /* track number */
227 if (taglib_tag_track(tag) > 0) {
228 asprintf(&(mi->cinfo[MI_CINFO_TRACK]), "%3i", taglib_tag_track(tag));
229 if (mi->cinfo[MI_CINFO_TRACK] == NULL)
230 err(1, "mi_extract: asprintf failed for CINFO_TRACK");
231 }
232
233 /* year */
234 if (taglib_tag_year(tag) > 0) {
235 asprintf(&(mi->cinfo[MI_CINFO_YEAR]), "%i", taglib_tag_year(tag));
236 if (mi->cinfo[MI_CINFO_YEAR] == NULL)
237 err(1, "mi_extract: asprintf failed for CINFO_YEAR");
238 }
239
240 /* playlength in seconds (will be 0 if unavailable) */
241 mi->length = taglib_audioproperties_length(properties);
242 if (mi->length > 0) {
243 if ((mi->cinfo[MI_CINFO_LENGTH] = strdup(time2str(mi->length))) == NULL)
244 err(1, "mi_extract: strdup failed for CINO_LENGTH");
245 }
246
247 /* record the time we extracted this info */
248 time(&mi->last_updated);
249
250 /* cleanup */
251 taglib_tag_free_strings();
252 taglib_file_free(file);
253
254 return mi;
255 }
256
257 /*****************************************************************************
258 * The sanitation routines
259 ****************************************************************************/
260 void
str_sanitize(char * s)261 str_sanitize(char *s)
262 {
263 size_t i;
264 char c;
265
266 for (i = 0; i < strlen(s); i++) {
267 c = s[i];
268 if (!isdigit(c) && !isalpha(c) && !ispunct(c) && c != ' ')
269 s[i] = '?';
270 }
271 }
272
273 void
mi_sanitize(meta_info * mi)274 mi_sanitize(meta_info *mi)
275 {
276 int i;
277 for (i = 0; i < MI_NUM_CINFO; i++) {
278 if (mi->cinfo[i] != NULL)
279 str_sanitize(mi->cinfo[i]);
280 }
281 }
282
283
284 /*****************************************************************************
285 * The mi_query_* and mi_compare stuff.
286 ****************************************************************************/
287
288 /* the global query description */
289 mi_query_description _mi_query;
290
291 /* global flag to indicate if we should match against filename in queires */
292 bool mi_query_match_filename;
293
294 /* initialize the query structures */
295 void
mi_query_init()296 mi_query_init()
297 {
298 int i;
299
300 for (i = 0; i < MI_MAX_QUERY_TOKENS; i++)
301 _mi_query.tokens[i] = NULL;
302
303 mi_query_match_filename = true;
304 _mi_query.raw = NULL;
305 _mi_query.ntokens = 0;
306 }
307
308 /* determine if a query has been set */
309 bool
mi_query_isset()310 mi_query_isset()
311 {
312 return _mi_query.ntokens != 0;
313 }
314
315 /* free the query structures */
316 void
mi_query_clear()317 mi_query_clear()
318 {
319 int i;
320
321 for (i = 0; i < MI_MAX_QUERY_TOKENS; i++) {
322 if (_mi_query.tokens[i] != NULL) {
323 free(_mi_query.tokens[i]);
324 _mi_query.tokens[i] = NULL;
325 }
326 }
327
328 if (_mi_query.raw != NULL) {
329 free(_mi_query.raw);
330 _mi_query.raw = NULL;
331 }
332
333 _mi_query.ntokens = 0;
334 }
335
336 /* add a token to the current query description */
337 void
mi_query_add_token(const char * token)338 mi_query_add_token(const char *token)
339 {
340 if (_mi_query.ntokens == MI_MAX_QUERY_TOKENS)
341 errx(1, "mi_query_add_token: reached shamefull limit");
342
343 /* match or no? */
344 if (token[0] == '!') {
345 _mi_query.match[_mi_query.ntokens] = false;
346 token++;
347 } else
348 _mi_query.match[_mi_query.ntokens] = true;
349
350 /* copy token */
351 if ((_mi_query.tokens[_mi_query.ntokens++] = strdup(token)) == NULL)
352 err(1, "mi_query_add_token: strdup failed");
353 }
354
355 void
mi_query_setraw(const char * query)356 mi_query_setraw(const char *query)
357 {
358 if (_mi_query.raw != NULL)
359 free(_mi_query.raw);
360
361 if ((_mi_query.raw = strdup(query)) == NULL)
362 err(1, "mi_query_setraw: query strdup failed");
363 }
364
365 const char *
mi_query_getraw()366 mi_query_getraw()
367 {
368 return _mi_query.raw;
369 }
370
371 /* match a given meta_info struct against the global query */
372 bool
mi_match(const meta_info * mi)373 mi_match(const meta_info *mi)
374 {
375 bool matches;
376 int i, j;
377
378 for (i = 0; i < _mi_query.ntokens; i++) {
379
380 matches = false;
381
382 /* does the filename match? */
383 if (mi_query_match_filename) {
384 if ((strcasestr(mi->filename, _mi_query.tokens[i])) != NULL)
385 matches = true;
386 }
387
388 /* do any of the CINFO fields match? */
389 for (j = 0; !matches && j < MI_NUM_CINFO; j++) {
390 if (mi->cinfo[j] == NULL)
391 continue;
392
393 if ((strcasestr(mi->cinfo[j], _mi_query.tokens[i])) != NULL)
394 matches = true;
395 }
396
397 if (!matches && _mi_query.match[i])
398 return false;
399 if (matches && !_mi_query.match[i])
400 return false;
401 }
402
403 return true;
404 }
405
406 /*
407 * Match any given string against the current query. Note that this is ONLY
408 * used when searching the library window.
409 */
410 bool
str_match_query(const char * s)411 str_match_query(const char *s)
412 {
413 bool matches;
414 int i;
415
416 for (i = 0; i < _mi_query.ntokens; i++) {
417 matches = false;
418 if (strcasestr(s, _mi_query.tokens[i]) != NULL)
419 matches = true;
420 if (!matches && _mi_query.match[i])
421 return false;
422 if (matches && !_mi_query.match[i])
423 return false;
424 }
425
426 return true;
427 }
428
429
430 /*****************************************************************************
431 * mi_sort_* and mi_compare stuff
432 ****************************************************************************/
433
434 /* global sort description */
435 mi_sort_description _mi_sort;
436
437 /* initialize the sort ordering to what i like */
438 void
mi_sort_init()439 mi_sort_init()
440 {
441 _mi_sort.order[0] = MI_CINFO_ARTIST;
442 _mi_sort.order[1] = MI_CINFO_ALBUM;
443 _mi_sort.order[2] = MI_CINFO_TRACK;
444 _mi_sort.order[3] = MI_CINFO_TITLE;
445
446 _mi_sort.descending[0] = false;
447 _mi_sort.descending[1] = false;
448 _mi_sort.descending[2] = false;
449 _mi_sort.descending[3] = false;
450
451 _mi_sort.nfields = 4;
452 }
453
454 /* clear the current sort */
455 void
mi_sort_clear()456 mi_sort_clear()
457 {
458 _mi_sort.nfields = 0;
459 }
460
461 /* Set the current sort description to what is provided in the given string.
462 * The format of the string is a comma-seperated list of field-names, possibly
463 * preceeded with a dash (-) to indicate that the field should be sorted
464 * descending.
465 * Example:
466 * s = "artist,album,track,title"
467 * Would set the global sort structure to sort meta_info structs by first
468 * artist, then album, then track, then title.
469 * Example:
470 * s = "artist,-year"
471 * Would set the global sort structure to sort meta_info struct first by
472 * artist name, then by year descending (showing the most recent work of
473 * a given artist first).
474 *
475 * RETURNS:
476 * 0 if s is parsed without error. 1 otherwise.
477 */
478 int
mi_sort_set(const char * s,const char ** errmsg)479 mi_sort_set(const char *s, const char **errmsg)
480 {
481 mi_sort_description new_sort;
482 bool found;
483 char *token;
484 char *line;
485 char *copy;
486 int idx;
487 int i;
488
489 /* error message to be returned */
490 const char *ERRORS[2] = {
491 "Too many fields",
492 "Unknown field name"
493 };
494 *errmsg = NULL;
495
496 if ((line = strdup(s)) == NULL)
497 err(1, "mi_sort_set: sort strdup failed");
498
499 idx = 0;
500 copy = line;
501
502 while ((token = strsep(&line, ",")) != NULL) {
503
504 if (strlen(token) == 0)
505 continue;
506
507 if (idx >= MI_NUM_CINFO) {
508 *errmsg = ERRORS[0];
509 goto err;
510 }
511
512 new_sort.descending[idx] = (token[0] == '-');
513 if (token[0] == '-')
514 token++;
515
516 found = false;
517 for (i = 0; i < MI_NUM_CINFO && !found; i++) {
518 if (strcasecmp(token, MI_CINFO_NAMES[i]) == 0) {
519 new_sort.order[idx] = i;
520 found = true;
521 }
522 }
523
524 if (!found) {
525 *errmsg = ERRORS[1];
526 goto err;
527 }
528
529 idx++;
530 }
531
532
533 /* copy new sort description into global one */
534 new_sort.nfields = idx;
535 for (idx = 0; idx < new_sort.nfields; idx++) {
536 _mi_sort.order[idx] = new_sort.order[idx];
537 _mi_sort.descending[idx] = new_sort.descending[idx];
538 }
539 _mi_sort.nfields = new_sort.nfields;
540
541 free(copy);
542 return 0;
543
544 err:
545 free(copy);
546 return 1;
547 }
548
549 /*
550 * Compare two meta_info structs using the global sort description
551 * Note that this function is suitable for passing to qsort(3) and the like.
552 * TODO investigate way to ignore stuff like a starting "The" or "A" when
553 * sorting. Wait, do I want this?
554 */
555 int
mi_compare(const void * A,const void * B)556 mi_compare(const void *A, const void *B)
557 {
558 int field;
559 int ret;
560 int i;
561
562 const meta_info **a2 = (const meta_info**) A;
563 const meta_info **b2 = (const meta_info**) B;
564 const meta_info *a = *a2;
565 const meta_info *b = *b2;
566
567 for (i = 0; i < _mi_sort.nfields; i++) {
568 field = _mi_sort.order[i];
569
570 if (a->cinfo[field] == NULL && b->cinfo[field] == NULL)
571 return 0;
572 if (a->cinfo[field] != NULL && b->cinfo[field] == NULL)
573 return (_mi_sort.descending[i] ? 1 : -1);
574 if (a->cinfo[field] == NULL && b->cinfo[field] != NULL)
575 return (_mi_sort.descending[i] ? -1 : 1);
576
577 ret = strcasecmp(a->cinfo[field], b->cinfo[field]);
578 if (ret != 0)
579 return (_mi_sort.descending[i] ? -1 * ret : ret);
580 }
581
582 return 0;
583 }
584
585
586 /*****************************************************************************
587 * mi_display_* stuff
588 ****************************************************************************/
589
590 /* global display description */
591 mi_display_description mi_display;
592
593 /* initialize global display description to what i like */
594 void
mi_display_init()595 mi_display_init()
596 {
597 mi_display.nfields = 6;
598
599 mi_display.order[0] = MI_CINFO_ARTIST;
600 mi_display.order[1] = MI_CINFO_ALBUM;
601 mi_display.order[2] = MI_CINFO_TITLE;
602 mi_display.order[3] = MI_CINFO_TRACK;
603 mi_display.order[4] = MI_CINFO_YEAR;
604 mi_display.order[5] = MI_CINFO_LENGTH;
605
606 mi_display.widths[0] = 20;
607 mi_display.widths[1] = 20;
608 mi_display.widths[2] = 20;
609 mi_display.widths[3] = 4;
610 mi_display.widths[4] = 4;
611 mi_display.widths[5] = 9;
612
613 mi_display.align[0] = LEFT;
614 mi_display.align[1] = LEFT;
615 mi_display.align[2] = LEFT;
616 mi_display.align[3] = RIGHT;
617 mi_display.align[4] = RIGHT;
618 mi_display.align[5] = RIGHT;
619 }
620
621 /* reset the display to what i like */
622 void
mi_display_reset()623 mi_display_reset()
624 {
625 mi_display_init();
626 }
627
628 /* return the total width of the current display description */
629 int
mi_display_getwidth()630 mi_display_getwidth()
631 {
632 int sum;
633 int i;
634
635 sum = 0;
636 for (i = 0; i < mi_display.nfields; i++)
637 sum += mi_display.widths[i] + 1;
638
639 return sum;
640 }
641
642 /* convert current display to a string for showing elsewhere */
643 char *
mi_display_tostr()644 mi_display_tostr()
645 {
646 /*
647 * NOTE: for the below "dirty hack" ... it would require some *huge*
648 * field-widths to overload this, since there can only be MI_NUM_CINFO
649 * fields. low-priority
650 */
651 static char s[1000]; /* XXX dirty hack for now */
652 char *c;
653 int field, cinfo, size, num;
654
655 size = sizeof(s);
656
657 c = s;
658 for (field = 0; field < mi_display.nfields && size > 0; field++) {
659
660 if (mi_display.align[field] == RIGHT) {
661 *c = '-';
662 c++;
663 }
664
665 cinfo = mi_display.order[field];
666
667 num = snprintf(c, size, "%s.%i%s",
668 MI_CINFO_NAMES[cinfo],
669 mi_display.widths[field],
670 (field + 1 == mi_display.nfields ? "" : ","));
671
672 size -= num;
673 c += num;
674 }
675
676 return s;
677 }
678
679 /*
680 * Set the global display description to the format specified in the given
681 * string.
682 */
683 int
mi_display_set(const char * display,const char ** errmsg)684 mi_display_set(const char *display, const char **errmsg)
685 {
686 mi_display_description new_display;
687 bool found;
688 char *s, *copy;
689 char *token;
690 char *num;
691 int idx;
692 int i;
693
694 /* error message to be returned */
695 const char *ERRORS[3] = {
696 "Too many fields",
697 "Missing '.' and size field",
698 "Unknown field name"
699 };
700
701 *errmsg = NULL;
702 new_display.nfields = 0;
703
704 if ((s = strdup(display)) == NULL)
705 err(1, "mi_display_set: display strdup failed");
706
707 copy = s;
708 idx = 0;
709
710 while ((token = strsep(&s, ",")) != NULL) {
711
712 if (strlen(token) == 0)
713 continue;
714
715 /* make sure we don't have too many fields */
716 if (idx >= MI_NUM_CINFO) {
717 *errmsg = ERRORS[0];
718 goto err;
719 }
720
721 /* determine alignment (if '-' is present it's right, otherwise left) */
722 if (token[0] == '-') {
723 new_display.align[idx] = RIGHT;
724 token++;
725 } else
726 new_display.align[idx] = LEFT;
727
728 /* make sure there's a number part to the field */
729 if ((num = strstr(token, ".")) == NULL) {
730 *errmsg = ERRORS[1];
731 goto err;
732 }
733
734 *num = '\0';
735 num++;
736
737 new_display.widths[idx] = strtonum(num, 1, 1000, errmsg);
738 if (*errmsg != NULL)
739 goto err;
740
741 /* get the field name */
742 found = false;
743 for (i = 0; i < MI_NUM_CINFO && !found; i++) {
744 if (strcasecmp(token, MI_CINFO_NAMES[i]) == 0) {
745 new_display.order[idx] = i;
746 found = true;
747 }
748 }
749
750 if (!found) {
751 *errmsg = ERRORS[2];
752 goto err;
753 }
754
755 idx++;
756 new_display.nfields++;
757 }
758
759 /* copy results into global display description */
760 for (idx = 0; idx < new_display.nfields; idx++) {
761 mi_display.order[idx] = new_display.order[idx];
762 mi_display.widths[idx] = new_display.widths[idx];
763 mi_display.align[idx] = new_display.align[idx];
764 }
765 mi_display.nfields = new_display.nfields;
766
767 free(copy);
768 return 0;
769
770 err:
771 free(copy);
772 return 1;
773 }
774