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