1 #include <cstdlib>
2 #include <cstdio>
3 #include <cstdarg>
4 #include <cstring>
5 #include <ctime>
6 #include <cctype>
7 #include <clocale>
8 #include <stdexcept>
9 #include <string>
10 #include <memory>
11 #include "setgroup.h"
12 #include "setid3.h"
13 #include "setfname.h"
14 #include "setquery.h"
15 #include "setid3v2.h"
16 #include "setlyr3.h"
17 #include "mass_tag.h"
18 #include "pattern.h"
19 #include "dumptag.h"
20 #ifdef _WIN32
21 # include <windows.h>
22 #endif
23
24 #define _version_ "0.80 (2015356)"
25
26 /*
27
28 copyright (c) 2004-2006, 2015 squell <squell@alumina.nl>
29
30 use, modification, copying and distribution of this software is permitted
31 under the conditions described in the file 'COPYING'.
32
33 */
34
35 namespace out = tag::write;
36 using namespace std;
37 using fileexp::mass_tag;
38 using tag::ID3field;
39 using tag::FIELD_MAX;
40
41 #if __cplusplus < 201103L
42 #define unique_ptr auto_ptr
43 #endif
44
45 /* ====================================================== */
46
47 // exitcodes: 0 - ok, 1 - syntax, 2 - errors, 3 - fatal errors
48
49 static const char* Name = "id3";
50 static int exitc = 0;
51
Copyright()52 static void Copyright()
53 {
54 // |=======================64 chars wide==========================|
55 printf(
56 "%s " _version_ ", Copyright (C) 2003, 04, 05, 06, 15 Marc R. Schoolderman\n"
57 "This program comes with ABSOLUTELY NO WARRANTY.\n\n"
58 "This is free software, and you are welcome to redistribute it\n"
59 "under certain conditions; see the file named COPYING in the\n"
60 "source distribution for details.\n",
61 Name
62 );
63 exit(exitc=1);
64 }
65
66 static const char* const Options[] = {
67 "v", "-verbose",
68 "d", "-delete",
69 "t", "-title",
70 "a", "-artist",
71 "l", "-album",
72 "n", "-track",
73 "y", "-year",
74 "g", "-genre",
75 "c", "-comment",
76 "D", "-duplicate",
77 "f", "-rename",
78 "q", "-query",
79 "m", "-match",
80 "R", "-recursive",
81 "M", "-keep-time",
82 "V", "-version",
83 "s", "-size",
84 "E", "-if-exists",
85 "u", "-update",
86 "r", "-remove=",
87 "w", "-frame=",
88 "1", "-id3v1",
89 "2", "-id3v2",
90 "3", "-lyrics3",
91 "?", "-help"
92 };
93
Help(bool long_opt=false)94 static void Help(bool long_opt=false)
95 {
96 const char*const* const flags = Options+long_opt;
97 printf(
98 "%s " _version_ "\n"
99 "usage: %s [-1 -2 -3] [OPTIONS] filespec ...\n"
100 " -%s\t\t" "give verbose output\n"
101 " -%s\t\t" "clear existing tag\n"
102 " -%s <title>\t" "set tag fields\n"
103 " -%s <artist>\t" "\n"
104 " -%s <album>\t" " (i'th matched `*' wildcard = %%1-%%9,%%0\n"
105 " -%s <tracknr>\t" " path/file name/counters = %%p %%f %%x %%X\n"
106 " -%s <year> \t" " value of tag field in file = %%t %%a %%l %%n %%y %%g %%c)\n"
107 " -%s <genre>\t" "\n"
108 " -%s <comment>\t" "\n"
109 " -%s <filename>\t" "copy tags read from filename\n"
110 " -%s <template>\t" "rename files according to template\n"
111 " -%s <format>\t" "print formatted string on standard output\n"
112 " -%s\t\t" "match variables in filespec\n"
113 " -%s\t\t" "search recursively\n"
114 " -%s\t\t" "preserve modification time of files\n"
115 " -%s\t\t" "print version info\n"
116 "Only on last selected tag type:\n"
117 " -%s <size> \t" "set tag size\n"
118 " -%s\t\t" "only write if tag already exists\n"
119 " -%s\t\t" "update all standard fields\n"
120 " -%sTYPE\t\t" "remove `TYPE' frames\n"
121 " -%sTYPE <data>\t" "write a `TYPE' frame\n"
122 "\nReport bugs to <squell@alumina.nl>.\n",
123 Name,
124 Name,
125 flags[ 0], flags[ 2], flags[ 4], flags[ 6], flags[ 8], flags[10], flags[12], flags[14], flags[16], flags[18],
126 flags[20], flags[22], flags[24], flags[26], flags[28], flags[30], flags[32], flags[34], flags[36], flags[38],
127 flags[40]
128 );
129 exit(exitc=1);
130 }
131
shelp(bool quit=true)132 static int shelp(bool quit = true)
133 {
134 fprintf(stderr, "Try `%s -h' or `%s --help' for more information.\n", Name, Name);
135 if(quit) exit (exitc=1);
136 else return(exitc=1);
137 }
138
eprintf(const char * msg,...)139 static void eprintf(const char* msg, ...)
140 {
141 exitc = 2;
142 va_list args;
143 va_start(args, msg);
144 fprintf (stderr, "%s: ", Name);
145 vfprintf (stderr, msg, args);
146 va_end(args);
147 }
148
cmdalias(const char * arg)149 static const char* cmdalias(const char* arg)
150 {
151 for(size_t i=1; i < sizeof Options/sizeof(const char*); i+=2) {
152 if(strcmp(arg,Options[i]) == 0) {
153 return Options[i-1];
154 }
155 }
156 eprintf("unrecognized switch `-%s'\n", arg);
157 return shelp(), arg;
158 }
159
argtol(const char * arg)160 static long argtol(const char* arg) // convert argument to long
161 {
162 char* endp;
163 long n = strtol(arg, &endp, 10);
164 if(*endp != '\0' || n < 0) {
165 eprintf("%s: invalid argument\n", arg);
166 exit(exitc=1);
167 }
168 return n;
169 }
170
argpath(char * arg)171 inline static char* argpath(char* arg)
172 {
173 #if defined(__DJGPP__) || defined(_WIN32)
174 for(char* p = arg; *p; ++p) // convert backslashes
175 if(*p == '\\') *p = '/';
176 #endif
177 return arg;
178 }
179
180 /* ====================================================== */
181
182 class verbose : public mass_tag {
183 public:
verbose(const tag::writer & write,const tag::reader & read)184 verbose(const tag::writer& write, const tag::reader& read)
185 : mass_tag(write, read) { }
186 static bool enable;
187 private:
188 static clock_t time;
189
190 struct timer {
timerverbose::timer191 timer() { time = clock(); }
~timerverbose::timer192 ~timer()
193 {
194 time = clock() - time;
195 if(enable) {
196 if(exitc!=0) fprintf(stderr, "Errors were encountered\n");
197 if(exitc!=1) fprintf(stderr, "(%lu files in %.3fs) done\n", mass_tag::total(), double(time) / CLOCKS_PER_SEC);
198 }
199 }
200 };
201 friend struct timer; // req by C++98
202
file(const char * name,const fileexp::record & f)203 virtual bool file(const char* name, const fileexp::record& f)
204 {
205 if(verbose::enable) {
206 static timer initialize;
207 if(counter==1 && name-f.path)
208 fprintf(stderr, "%.*s\n", int(name-f.path), f.path);
209 fprintf(stderr, "\t%s\n", name);
210 }
211 if(! mass_tag::file(name, f) )
212 eprintf("could not edit tag in %s\n", f.path);
213 return 1;
214 }
215 };
216
217 bool verbose::enable;
218 clock_t verbose::time;
219
220 /* ====================================================== */
221
222 namespace op {
223
224 enum { // state information bitset
225 no_op = 0x00,
226 recur = 0x01, // work recursively?
227 w = 0x02, // write requested?
228 ren = 0x04, // rename requested?
229 rd = 0x08, // read requested?
230 clobr = 0x10, // clear requested?
231 patrn = 0x20 // match requested?
232 };
233 typedef int oper_t;
234
235 template<class T> struct box { T object; };
use(box<T> & x)236 template<class T> T& use(box<T>& x) { return x.object; }
use(T & x)237 template<class T> T& use(T& x) { return x; }
238
239 struct tag_info :
240 out::query,
241 box<out::ID3>,
242 box<out::ID3v2>,
243 box<out::Lyrics3>,
244 box< tag::combined< tag::reader > >,
245 tag::reader,
246 out::file,
247 fileexp::find
248 {
249 template<class T>
enableop::tag_info250 T& enable()
251 {
252 T& selected = use<T>(*this);
253 use< tag::combined<tag::handler> >(*this).with(selected);
254 use< tag::combined<tag::reader> >(*this).with(selected);
255 return selected;
256 }
257
readop::tag_info258 tag::metadata* read(const char* fn) const
259 { return box< tag::combined<tag::reader> >::object.read(fn); }
260
fileop::tag_info261 bool file(const char*, const fileexp::record& rec)
262 {
263 tag::output(use< tag::combined<tag::reader> >(*this), rec.path, stdout);
264 return true;
265 }
266 };
267
268 }
269
270 /* ====================================================== */
271
272 // tag lister
273
274 struct listtag : fileexp::find {
listtaglisttag275 listtag(tag::reader& in) : m_reader(in)
276 { }
277 tag::reader& m_reader;
278
contentlisttag279 static void content(const char* fmt, tag::metadata::value_string data)
280 {
281 if(data.good()) {
282 string str = data;
283 for(string::iterator p = str.begin(); p != str.end(); ++p)
284 if(std::iscntrl(*p)) *p = ' ';
285 printf(fmt, str.c_str());
286 }
287 }
288
filelisttag289 virtual bool file(const char*, const fileexp::record& rec)
290 {
291 using namespace tag;
292 std::unique_ptr<metadata> ptr( m_reader.read(rec.path) );
293 if(ptr.get()) {
294 const metadata& tag = *ptr;
295 printf("File: %s\n", rec.path);
296 if(tag) {
297 content("Metadata: %s\n",tag[FIELD_MAX]);
298 content("Title: %s\n", tag[title]);
299 content("Artist: %s\n", tag[artist]);
300 content("Album: %s\n", tag[album]);
301 content("Track: %s\n", tag[track]);
302 content("Year: %s\n", tag[year]);
303 content("Genre: %s\n", tag[genre]);
304 content("Comment: %s\n", tag[cmnt]);
305 } else {
306 content("Metadata: %s\n", "none found");
307 }
308 printf("\n");
309 }
310 return true;
311 }
312 };
313
314 // performs the selected operations on the file arguments
315
process_(fileexp::find & work,char * files[],bool recur)316 int process_(fileexp::find& work, char* files[], bool recur)
317 {
318 do {
319 if(! work.glob(argpath(*files), recur) )
320 eprintf("no files matching %s\n", *files);
321 } while(*++files);
322 return exitc;
323 }
324
325 // contains CLI interface loop
326
main_(int argc,char * argv[])327 int main_(int argc, char *argv[])
328 {
329 op::tag_info tag;
330
331 ID3field field;
332 const char* val[FIELD_MAX] = { 0, }; // fields to alter
333
334 tag::handler* chosen = 0; // pointer to last enabled
335 const char* copyfn = 0; // alternate from-file
336
337 using namespace op;
338 char none[] = "";
339
340 enum parm_t { // parameter modes
341 no_value, force_fn,
342 std_field, custom_field, suggest_size,
343 set_rename, set_query, set_copyfrom
344 };
345
346 parm_t cmd = no_value;
347 oper_t state = no_op;
348 char* opt = none; // used for command stacking
349
350 for(int i=1; i < argc; i++) {
351 switch( cmd ) {
352 case no_value: // process a command argument
353 if(*opt == '\0' && argv[i][0] == '-')
354 opt = argv[i]+1;
355 else --i; // stash argument for later
356
357 switch( *opt++ ) {
358 case 'v': verbose::enable=1; break;
359 case 'M': tag.touch(false); break;
360 case 'f': cmd = set_rename; break;
361 case 'q': cmd = set_query; break;
362
363 case 'm': if(!(state & recur)) {
364 state |= patrn; break;
365 } else if(false) // skip next statement
366 case 'R': if(!(state & patrn)) {
367 state |= recur; break;
368 }
369 eprintf("cannot use -R and -m at the same time\n");
370 shelp();
371
372 case 'D': if(!(state&clobr)) {
373 cmd = set_copyfrom; break;
374 }
375 case 'd': if(!(state&clobr)) {
376 state |= (w|clobr); break;
377 }
378 eprintf("cannot use either -d or -D more than once\n");
379 shelp();
380
381 default:
382 field = mass_tag::field(opt[-1]);
383 if(field == FIELD_MAX) {
384 char tmp[2] = { opt[-1] };
385 cmdalias(tmp); // will produce error
386 }
387 cmd = std_field; break;
388 case '3':
389 chosen = &tag.enable<out::Lyrics3>().create();
390 tag.with(use<out::ID3>(tag)).create();
391 break;
392 case '1':
393 chosen = &tag.enable<out::ID3>().create();
394 break;
395 case '2':
396 chosen = &tag.enable<out::ID3v2>().create();
397 break;
398 case '0':
399 chosen = &tag; // "null" tag - does nothing
400 break;
401
402 case 's': // tag specific switches
403 if(chosen) {
404 if(*opt == '\0')
405 cmd = suggest_size;
406 else {
407 long n = argtol(opt);
408 chosen->reserve(n);
409 state |= w;
410 }
411 opt = none;
412 break;
413 }
414 case 'w':
415 if(chosen) {
416 cmd = custom_field;
417 break;
418 }
419 case 'r':
420 if(chosen) {
421 if(! chosen->rm(opt) ) {
422 eprintf("selected tag does not have `%s' frames\n", opt);
423 shelp();
424 }
425 state |= w;
426 opt = none;
427 break;
428 }
429 case 'u':
430 if(chosen) {
431 for(int i = 0; i < FIELD_MAX; ++i)
432 chosen->set(ID3field(i), mass_tag::var(i));
433 state |= w;
434 break;
435 }
436
437 case 'E':
438 if(chosen) {
439 chosen->create(false);
440 break;
441 }
442
443 eprintf("specify tag format before -%c\n", opt[-1]);
444 shelp();
445
446 case '?': Help(1);
447 case 'h': Help();
448 case 'V': Copyright();
449
450 case '-':
451 if(opt-2 != argv[i]) {
452 cmdalias(opt-2); // will produce error
453 } else if(*opt == '\0') {
454 case '\0': // end of switches
455 cmd = force_fn;
456 } else { // --long-option
457 char *sep = strchr(opt, '=');
458 if(sep) {
459 const char save = sep[1];
460 sep[1] = '\0'; // this is a kludge
461 sep[0] = *cmdalias(argv[i]+1);
462 sep[1] = save;
463 *(argv[i] = sep-1) = '-';;
464 } else {
465 strcpy(argv[i]+1, cmdalias(argv[i]+1));
466 }
467 --i;
468 }
469 opt = none;
470 }
471 continue;
472
473 case std_field: // write a standard field
474 val[field] = argv[i];
475 break;
476
477 case set_copyfrom: // specify source tag
478 copyfn = argv[i];
479 state |= clobr;
480 break;
481
482 case custom_field: // v2 - write a custom field
483 if(! chosen->set(opt, argv[i]) ) {
484 eprintf("writing `%s' frames is not supported\n", opt);
485 shelp();
486 }
487 opt = none;
488 break;
489
490 case suggest_size: // v2 - suggest size
491 chosen->reserve( argtol(argv[i]) );
492 break;
493
494 case set_rename: // specify rename format
495 if(strrchr(argv[i],'/')) {
496 eprintf("will not rename across directories\n");
497 shelp();
498 } else if(*argv[i] == '\0') {
499 eprintf("empty format string rejected\n");
500 shelp();
501 } else
502 tag.rename( argpath(argv[i]) );
503 state |= ren;
504 cmd = no_value;
505 continue;
506
507 case set_query: // specify echo format
508 tag.print( argv[i] );
509 state |= rd;
510 cmd = no_value;
511 continue;
512
513 case force_fn: // argument is filespec
514 if(!chosen) { // use default tags
515 tag.enable<out::ID3v2>();
516 tag.enable<out::Lyrics3>();
517 tag.enable<out::ID3>().create();
518 }
519
520 for(int f = 0; f < FIELD_MAX; ++f) // propagate general ops
521 if(val[f]) tag.set(ID3field(f), val[f]);
522 if(copyfn && !tag.from(copyfn))
523 eprintf("note: could not read tags from %s\n", copyfn);
524 if(state & clobr)
525 tag.rewrite();
526
527 if(state & patrn) {
528 if(argv[i+1]) {
529 eprintf("-m %s: no file arguments are allowed\n", argv[i]);
530 shelp();
531 }
532 pattern spec(tag, argpath(argv[i]));
533 if(spec.vars() > 0) state |= w;
534 strcpy(argv[i], spec.c_str());
535 }
536
537 tag::writer* selected = &use<out::file>(tag);
538 tag::reader* source = &tag;
539
540 switch(state & (w|rd|ren)) {
541 case op::rd:
542 selected = &use<out::query>(tag);
543 case op::ren:
544 tag.ignore(0, tag.size()); // don't perform no-ops
545 case op::w | op::ren:
546 case op::w:
547 break;
548 default:
549 eprintf("cannot combine -q with any modifying operation\n");
550 shelp();
551 case op::no_op:
552 if(verbose::enable)
553 return process_(tag, &argv[i], state & recur);
554 listtag viewer(*source);
555 return process_(viewer, &argv[i], state & recur);
556 }
557
558 verbose tagger(*selected, *source);
559 return process_(tagger, &argv[i], state & recur);
560 }
561
562 state |= w; // set operation done flag
563 cmd = no_value;
564 }
565
566 eprintf("missing file arguments\n");
567 if(state == no_op) shelp();
568
569 return exitc;
570 }
571
572 // function-try blocks are not supported on some compilers (borland),
573 // so this little de-tour is necessary
574
main(int argc,char * argv[])575 int main(int argc, char *argv[])
576 {
577 if(char* prog = argv[0]) { // set up program name
578 if(char* p = strrchr(argpath(prog), '/')) prog = p+1;
579 #if defined(__DJGPP__) || defined(_WIN32)
580 char* end = strchr(prog, '\0'); // make "unixy" in appearance
581 for(char* p = prog; p != end; ++p) *p = tolower(*p);
582 if(end-prog > 4 && strcmp(end-4, ".exe") == 0) end[-4] = '\0';
583 #endif
584 Name = prog;
585 }
586 try {
587 # if defined(_WIN32) // set up locale
588 char codepage[12];
589 sprintf(codepage, ".%d", GetACP() & 0xFFFF);
590 setlocale(LC_CTYPE, codepage);
591 struct chcp { // fiddle with the console fonts
592 int const cp_in, cp_out;
593 chcp(int cp_new = GetACP())
594 : cp_in(GetConsoleCP()), cp_out(GetConsoleOutputCP())
595 { SetConsoleCP(cp_new), SetConsoleOutputCP(cp_new); }
596 ~chcp()
597 { SetConsoleCP(cp_in), SetConsoleOutputCP(cp_out); }
598 } lock;
599 # else
600 setlocale(LC_CTYPE, "");
601 if(mblen(0,0) != 0)
602 setlocale(LC_CTYPE, "C");
603 # endif
604 return main_(argc, argv);
605 } catch(const tag::failure& f) {
606 eprintf("%s (tagging aborted)\n", f.what());
607 } catch(const out_of_range& x) {
608 eprintf("%s\n", x.what());
609 return shelp(0);
610 } catch(const exception& exc) {
611 eprintf("unhandled exception: %s\n", exc.what());
612 } catch(...) {
613 eprintf("unexpected unhandled exception\n");
614 }
615 return 3;
616 }
617