15 #include <sys/types.h>
19 #include "libdbg/dbglog.h"
23 ////////////////////////////////////////////////////////////////////////////////
31 static dirlist_t
*direxcept
= NULL
;
34 ////////////////////////////////////////////////////////////////////////////////
40 uint16_t year
; // can be 0
41 uint8_t track
; // can be 0
42 char *artist
; // can't be NULL
43 char *album
; // can't be NULL
44 char *title
; // can't be NULL
45 char *genre
; // can be NULL
49 ////////////////////////////////////////////////////////////////////////////////
50 static inline char *dupstrn (const void *ptr
, int size
) {
51 char *res
= malloc(size
+1);
52 if (size
> 0) memcpy(res
, ptr
, size
);
58 #define GETFIELDS(tagname,fldname) do { \
59 if (strcmp(nm, tagname) == 0) { \
60 if (fi->fldname != NULL) free(fi->fldname); \
61 fi->fldname = dupstrn(p, sz); \
67 #define GETFIELDN(tagname,fldname,nsz) do { \
68 if (strcmp(nm, tagname) == 0) { \
69 if (sz != nsz) goto error; \
70 memcpy(&fi->fldname, p, nsz); \
76 // and skip to next one
77 static int finfo_read (FILE *fl
, tagfile_info_t
*fi
) {
79 static char buf
[65536], nm
[65536];
81 memset(fi
, 0, sizeof(*fi
));
82 if (fread(&size
, 2, 1, fl
) != 1 || size
< 1) return -1;
83 if (fread(buf
, size
, 1, fl
) != 1) return -1;
90 if (sz
> size
) goto error
;
98 if (size
< 2) goto error
;
102 if (sz
> size
) goto error
;
105 GETFIELDS("filename", filename
);
106 GETFIELDS("artist", artist
);
107 GETFIELDS("album", album
);
108 GETFIELDS("title", title
);
109 GETFIELDS("genre", genre
);
110 GETFIELDN("inode", inode
, 8);
111 GETFIELDN("size", size
, 8);
112 GETFIELDN("mtime", mtime
, 8);
113 GETFIELDN("year", year
, 2);
114 GETFIELDN("track", track
, 1);
118 if (fi
->filename
== NULL
) goto error
;
119 if (size
!= 0) goto error
;
122 if (fi
->genre
!= NULL
) free(fi
->genre
);
123 if (fi
->title
!= NULL
) free(fi
->title
);
124 if (fi
->album
!= NULL
) free(fi
->album
);
125 if (fi
->artist
!= NULL
) free(fi
->artist
);
126 if (fi
->filename
!= NULL
) free(fi
->filename
);
127 memset(fi
, 0, sizeof(*fi
));
132 static void finfo_clear (tagfile_info_t
*fi
) {
134 if (fi
->genre
!= NULL
) free(fi
->genre
);
135 if (fi
->title
!= NULL
) free(fi
->title
);
136 if (fi
->album
!= NULL
) free(fi
->album
);
137 if (fi
->artist
!= NULL
) free(fi
->artist
);
138 if (fi
->filename
!= NULL
) free(fi
->filename
);
139 memset(fi
, 0, sizeof(*fi
));
144 ////////////////////////////////////////////////////////////////////////////////
145 static tagfile_info_t
*filelist
= NULL
;
146 static int fcount
= 0;
147 static int fallocated
= 0;
150 static void flist_clear (void) {
151 if (filelist
!= NULL
) {
152 for (int f
= fcount
-1; f
>= 0; --f
) finfo_clear(&filelist
[f
]);
154 fcount
= fallocated
= 0;
159 static tagfile_info_t
*add_fname (const char *name
, int doadd
, int *isnew
) {
161 if (name
== NULL
|| !name
[0]) abort();
163 int imax
= fcount
-1, cmp
;
164 // continually narrow search until just one element remains
165 while (imin
< imax
) {
166 int imid
= (imin
+imax
)/2; // we will never overflow here; no, really!
167 //assert(imid < imax);
168 // note: 0 <= imin < imax implies imid will always be less than imax
170 if (strcmp(filelist
[imid
].filename
, name
) < 0) imin
= imid
+1; else imax
= imid
;
173 // if A[] is empty, then imax < imin
174 // otherwise imax == imin
175 //if (imax != imin || imin < 0 || imin >= fcount) abort(); // the thing that should not be!
176 // deferred test for equality
177 if ((cmp
= strcmp(filelist
[imin
].filename
, name
)) == 0) {
178 if (isnew
!= NULL
) *isnew
= 0;
179 return &filelist
[imin
]; // got it!
181 imin
+= (cmp
< 0 ? 1 : 0); // newpos
183 if (isnew
!= NULL
) *isnew
= 1;
186 if (fcount
+1 > fallocated
) {
187 int newsz
= ((fallocated
+1)|0xfff)+1;
188 tagfile_info_t
*nfl
= realloc(filelist
, sizeof(filelist
[0])*newsz
);
189 if (nfl
== NULL
) { fprintf(stderr
, "FATAL: out of memory!\n"); abort(); }
193 //fprintf(stderr, " fcount=%d; insertto=%d\n", fcount, imin);
195 for (int f
= fcount
; f
> imin
; --f
) filelist
[f
] = filelist
[f
-1];
197 memset(&filelist
[imin
], 0, sizeof(filelist
[imin
]));
198 return &filelist
[imin
];
200 return NULL
; // not found
204 ////////////////////////////////////////////////////////////////////////////////
205 static int load_tagfile (const char *fname
) {
209 fl
= fopen(fname
, "r");
210 if (fl
== NULL
) return 0; // no tags yet
211 if (fread(sign
, 4, 1, fl
) != 1) goto error
;
212 if (memcmp(sign
, "TFI0", 4) != 0) goto error
;
213 if (fread(&fc
, 4, 1, fl
) != 1) goto error
;
214 printf("loading %u files...\n", (unsigned int)fc
);
216 tagfile_info_t fi
, *newfi
;
219 if (finfo_read(fl
, &fi
) != 0) goto error
;
220 if (stat(fi
.filename
, &st
) != 0) { finfo_clear(&fi
); continue; }
221 if (!S_ISREG(st
.st_mode
)) { finfo_clear(&fi
); continue; }
222 if (st
.st_ino
!= fi
.inode
|| st
.st_mtime
!= fi
.mtime
|| st
.st_size
!= fi
.size
) { finfo_clear(&fi
); continue; } // file was changed, drop and rescan
223 newfi
= add_fname(fi
.filename
, 1, &isnew
);
224 if (!isnew
) finfo_clear(newfi
);
228 printf("%d files loaded\n", fcount
);
237 ////////////////////////////////////////////////////////////////////////////////
238 static int write_str (FILE *fo
, const char *str
) {
241 if (str
== NULL
) abort();
243 if (len
> 4095) len
= 4095;
245 if (fwrite(&ln
, 2, 1, fo
) != 1) return -1;
246 if (ln
> 0 && fwrite(str
, ln
, 1, fo
) != 1) return -1;
251 static int write_strtag (FILE *fo
, const char *name
, const char *value
) {
253 if (write_str(fo
, name
) < 0) return -1;
254 if (write_str(fo
, value
) < 0) return -1;
260 static int write_numtag (FILE *fo
, const char *name
, const void *value
, size_t sz
) {
261 const uint8_t *v
= (const uint8_t *)value
;
263 for (size_t f
= 0; f
< sz
; ++f
) if (v
[f
] != 0) { allzero
= 0; break; }
266 if (write_str(fo
, name
) < 0) return -1;
267 if (fwrite(&wsz
, 2, 1, fo
) != 1) return -1;
268 if (fwrite(value
, sz
, 1, fo
) != 1) return -1;
274 static int write_tagfile (const char *filename
) {
276 uint32_t fc
= fcount
;
277 fo
= fopen(filename
, "w");
278 if (fo
== NULL
) return -1;
279 if (fwrite("TFI0", 4, 1, fo
) != 1) goto error
;
280 if (fwrite(&fc
, 4, 1, fo
) != 1) goto error
;
281 for (int f
= 0; f
< fcount
; ++f
) {
282 const tagfile_info_t
*fi
= &filelist
[f
];
283 long pos
= ftell(fo
), epos
;
285 if (fwrite(&size
, 2, 1, fo
) != 1) goto error
; // will be fixed later
286 if (write_strtag(fo
, "filename", fi
->filename
) < 0) goto error
;
287 if (write_strtag(fo
, "artist", fi
->artist
) < 0) goto error
;
288 if (write_strtag(fo
, "album", fi
->album
) < 0) goto error
;
289 if (write_strtag(fo
, "title", fi
->title
) < 0) goto error
;
290 if (write_strtag(fo
, "genre", fi
->genre
) < 0) goto error
;
291 if (write_numtag(fo
, "year", &fi
->year
, 2) < 0) goto error
;
292 if (write_numtag(fo
, "track", &fi
->track
, 1) < 0) goto error
;
293 if (write_numtag(fo
, "inode", &fi
->inode
, 8) < 0) goto error
;
294 if (write_numtag(fo
, "size", &fi
->size
, 8) < 0) goto error
;
295 if (write_numtag(fo
, "mtime", &fi
->mtime
, 8) < 0) goto error
;
297 if (epos
-(pos
+2) > 65535) goto error
;
299 if (fseek(fo
, pos
, SEEK_SET
) < 0) goto error
;
300 if (fwrite(&size
, 2, 1, fo
) != 1) goto error
;
301 if (fseek(fo
, epos
, SEEK_SET
) < 0) goto error
;
311 ////////////////////////////////////////////////////////////////////////////////
312 static char *w2u (const char *str
, int justcheck
) {
315 char *outs
, *ibuf
, *obuf
, *res
;
317 if (str
== NULL
) return (justcheck
? "" : strdup(""));
318 if (str
== NULL
|| !str
[0]) return (justcheck
? (char *)str
: strdup(str
));
319 for (const unsigned char *u
= (const unsigned char *)str
; *u
; ++u
) if (*u
>= 128) { asis
= 0; break; }
320 if (asis
) return (justcheck
? (char *)str
: strdup(str
));
321 cd
= iconv_open("utf-8", "cp1251");
322 if (cd
== (iconv_t
)-1) return NULL
;
323 outs
= malloc(strlen(str
)*6+4);
332 il
= iconv(cd
, &ibuf
, &il
, &obuf
, &ol
);
334 if (il
== (size_t)-1) {
338 res
= calloc(ool
-ol
+1, 1);
339 if (ool
-ol
> 0) memcpy(res
, outs
, ool
-ol
);
341 if (justcheck
) free(res
);
346 ////////////////////////////////////////////////////////////////////////////////
347 static char *str_convert (const char *str
, const char *def
, const char *fname
, int toutf
) {
348 const char *ostr
= str
;
350 int prevwasspace
= 0;
351 if (str
== NULL
|| !str
[0]) {
352 if (def
== NULL
) return NULL
; // skip this tag
356 char *t
= w2u(str
, 0);
358 dlogf("FUCK! %s: [%s]\n", fname
, str
);
361 //printf("u: [%s] -> [%s]\n", str, t);
364 s
= ss
= alloca(strlen(str
)+1);
365 while (*str
&& isspace(*str
)) ++str
;
367 if (!isspace(*str
)) {
379 while (*s
&& isspace(s
[strlen(s
)-1])) s
[strlen(s
)-1] = 0;
380 if (strcmp(s
, ostr
) != 0) dlogf("%s: [%s] -> [%s]\n", fname
, ostr
, s
);
381 if (toutf
) free((void *)ostr
);
382 if (!s
[0]) return (def
!= NULL
? strdup(def
) : NULL
);
387 ////////////////////////////////////////////////////////////////////////////////
388 static const unsigned char utf8Length
[256] = {
389 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x00-0x0f
390 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x10-0x1f
391 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x20-0x2f
392 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x30-0x3f
393 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x40-0x4f
394 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x50-0x5f
395 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x60-0x6f
396 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x70-0x7f
397 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, //0x80-0x8f
398 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, //0x90-0x9f
399 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, //0xa0-0xaf
400 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, //0xb0-0xbf
401 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, //0xc0-0xcf c0-c1: overlong encoding: start of a 2-byte sequence, but code point <= 127
402 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, //0xd0-0xdf
403 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, //0xe0-0xef
404 4,4,4,4,4,8,8,8,8,8,8,8,8,8,8,8 //0xf0-0xff
408 static inline int is_valid_utf8 (const void *buf
) {
409 const unsigned char *data
= (const unsigned char *)buf
;
411 unsigned char len
= utf8Length
[*data
];
417 case 8: case 9: // invalid
421 uc
= (*data
++)&(0x7c>>len
);
423 if (!data
[0]) return 0;
424 if (utf8Length
[*data
] != 9) return 0;
425 uc
= (uc
<<6)|((*data
++)&0x3f);
427 if (uc
> 0x10ffff) return 0;
428 if ((uc
>= 0xd800 && uc
<= 0xdfff) || // utf16/utf32 surrogates
429 (uc
>= 0xfdd0 && uc
<= 0xfdef) || // just for fun
430 (uc
>= 0xfffe && uc
<= 0xffff)) return 0; // bad unicode
436 static inline int notutf (const char *str
) {
438 for (const unsigned char *s
= (const unsigned char *)str
; *s
; ++s
) if (*s
>= 128) { havehi
= 1; break; }
440 //printf("[%s]: %d\n", str, is_valid_utf8(str));
441 return (is_valid_utf8(str
) == 0);
447 ////////////////////////////////////////////////////////////////////////////////
448 static int fill_tags (const char *filename
, tagfile_info_t
*fi
) {
451 //const TagLib_AudioProperties *properties;
453 const char *ext
= strrchr(filename
, '.');
456 if (ext
== NULL
|| (strcasecmp(ext
, ".mp3") == 0)) {
457 taglib_set_strings_unicode(0);
460 taglib_set_strings_unicode(1);
463 file
= taglib_file_new(filename
);
464 if (file
== NULL
) return 0;
465 if (stat(filename
, &st
) != 0) {
466 taglib_tag_free_strings();
467 taglib_file_free(file
);
470 tag
= taglib_file_tag(file
);
474 if (!dotest
) goto doutf8
;
475 if (notutf(taglib_tag_artist(tag
)) ||
476 notutf(taglib_tag_album(tag
)) ||
477 notutf(taglib_tag_title(tag
)) ||
478 notutf(taglib_tag_genre(tag
))) {
479 if (w2u(taglib_tag_artist(tag
), 1) == NULL
) goto doutf8
;
480 if (w2u(taglib_tag_album(tag
), 1) == NULL
) goto doutf8
;
481 if (w2u(taglib_tag_title(tag
), 1) == NULL
) goto doutf8
;
482 if (w2u(taglib_tag_genre(tag
), 1) == NULL
) goto doutf8
;
486 taglib_tag_free_strings();
487 taglib_file_free(file
);
488 taglib_set_strings_unicode(1);
489 file
= taglib_file_new(filename
);
490 if (file
== NULL
) return 0;
491 tag
= taglib_file_tag(file
);
493 taglib_tag_free_strings();
494 taglib_file_free(file
);
500 memset(fi
, 0, sizeof(*fi
));
501 //TODO: check for memory errors!
502 fi
->filename
= strdup(filename
);
503 fi
->inode
= st
.st_ino
;
504 fi
->size
= st
.st_size
;
505 fi
->mtime
= st
.st_mtime
;
506 fi
->artist
= str_convert(taglib_tag_artist(tag
), NULL
, filename
, toutf
);
507 fi
->album
= str_convert(taglib_tag_album(tag
), NULL
, filename
, toutf
);
508 fi
->title
= str_convert(taglib_tag_title(tag
), NULL
, filename
, toutf
);
509 fi
->genre
= str_convert(taglib_tag_genre(tag
), NULL
, filename
, toutf
);
510 fi
->year
= taglib_tag_year(tag
);
513 if (fi
->year
< 30) fi
->year
+= 2000;
514 else if (fi
->year
< 100) fi
->year
+= 1900;
515 else if (fi
->year
< 1930) fi
->year
= 0;
518 fi
->track
= (taglib_tag_track(tag
) < 256 ? taglib_tag_track(tag
) : 0);
524 properties = taglib_file_audioproperties(file);
525 if (properties != NULL) {
526 int seconds = taglib_audioproperties_length(properties)%60;
527 int minutes = (taglib_audioproperties_length(properties)-seconds)/60;
528 printf("-- AUDIO --\n");
529 printf("bitrate : %d\n", taglib_audioproperties_bitrate(properties));
530 printf("sample rate: %d\n", taglib_audioproperties_samplerate(properties));
531 printf("channels : %d\n", taglib_audioproperties_channels(properties));
532 printf("length : %d:%02i\n", minutes, seconds);
536 taglib_tag_free_strings();
537 taglib_file_free(file
);
542 ////////////////////////////////////////////////////////////////////////////////
543 // 0: normal file; 1: dir; <0: error
545 static inline int getftype (const char *fname) {
547 if (stat(fname, &st) == 0) {
548 if (S_ISDIR(st.st_mode)) return 1;
549 if (S_ISREG(st.st_mode)) return 0;
556 static void process_dir (const char *path
) {
558 if (stat(path
, &st
) == 0 && S_ISDIR(st
.st_mode
)) {
559 DIR *dp
= opendir(path
);
565 static char ppbuf
[PATH_MAX
+1];
566 char *pp
= realpath(path
, ppbuf
);
567 if (pp
== NULL
|| pp
[0] != '/') {
568 fprintf(stderr
, "FATAL: realpath(\"%s\") fucked! [%s]\n", path
, pp
);
571 //rd = malloc(strlen(pp)+4);
572 rd
= alloca(strlen(pp
)+4);
574 if (rd
[strlen(rd
)-1] != '/') strcat(rd
, "/");
575 HASH_FIND(hh
, direxcept
, &st
.st_ino
, sizeof(st
.st_ino
), exc
);
577 printf("***SKIP: [%s]\n", rd
);
581 // add this dir to 'exception list', so we will not loop
582 exc
= malloc(sizeof(*exc
));
583 exc
->inode
= st
.st_ino
;
584 HASH_ADD_KEYPTR(hh
, direxcept
, &exc
->inode
, sizeof(exc
->inode
), exc
);
586 //fn = malloc(strlen(rd)+8192);
587 fn
= alloca(strlen(rd
)+257);
589 printf("scanning: [%s] (%d)\n", rd
, fcount
);
590 while ((de
= readdir(dp
)) != NULL
) {
592 if (de
->d_name
[0] == '.' && (!de
->d_name
[1] || (de
->d_name
[1] == '.' && !de
->d_name
[2]))) continue;
593 if (!de
->d_name
[0]) continue; // just in case
594 sprintf(fn
, "%s%s", rd
, de
->d_name
);
595 if (stat(fn
, &st
) != 0) continue;
596 if (S_ISDIR(st
.st_mode
)) {
598 //fprintf(stderr, "[%s]\n", fn);
600 } else if (S_ISREG(st
.st_mode
)) {
601 tagfile_info_t
*fi
= add_fname(fn
, 0, NULL
);
605 int res
= fill_tags(fn
, &i
);
606 if (res
< 0) { fprintf(stderr
, "FATAL: writing error!\n"); abort(); }
608 fi
= add_fname(fn
, 1, NULL
);
622 ////////////////////////////////////////////////////////////////////////////////
623 int main (int argc
, char *argv
[]) {
624 dbglog_set_screenout(0);
625 dbglog_set_fileout(0);
627 for (int f
= 2; f
< argc
; ++f
) {
628 if (strcmp(argv
[f
], "--except") == 0) {
631 fprintf(stderr
, "FATAL: except what?\n");
634 if (stat(argv
[f
+1], &st
) == 0 && S_ISDIR(st
.st_mode
)) {
636 t
= malloc(sizeof(*t
));
637 t
->inode
= st
.st_ino
;
638 HASH_ADD_KEYPTR(hh
, direxcept
, &t
->inode
, sizeof(t
->inode
), t
);
640 for (int c
= f
+2; c
< argc
; ++c
) argv
[c
-2] = argv
[c
];
647 fprintf(stderr
, "WTF?!\n");
651 if (load_tagfile(argv
[1]) != 0) { fprintf(stderr
, "FATAL: can't open tagfile!\n"); return -1; }
655 //taglib_set_strings_unicode(1); // use UTF8
656 taglib_set_strings_unicode(0); // use UTF8
657 //taglib_id3v2_set_default_text_encoding(TagLib_ID3v2_UTF8); // used only for writing
659 for (int f
= 2; f
< argc
; ++f
) process_dir(argv
[f
]);
660 write_tagfile(argv
[1]);
661 printf("%d new files processed\n", fcount
-oldfc
);
665 for (int f = 0; f < fcount; ++f) {
666 const tagfile_info_t *tfi = &filelist[f];
667 if (tfi->artist != NULL && strcasecmp(tfi->artist, "lustre") == 0) {
668 printf("lustre: [%s]\n", tfi->filename);