1 #ifdef HAVE_CONFIG_H
2 #include "config.h"
3 #endif
4 //apparantly some systems, such as cygwin, do not have getopt defined in unistd
5 #ifdef HAVE_GETOPT_H
6 #include <getopt.h>
7 #endif
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 #include <ctype.h>
12 #ifdef HAVE_LIBREADLINE
13 #ifdef OLD_READLINE
14 extern "C" {
15 #endif
16 #include <readline/readline.h>
17 #include <readline/history.h>
18 #ifdef OLD_READLINE
19 }
20 #endif
21 #ifdef HAVE_LIBTERMCAP
22 #include <termcap.h>
23 #endif
24 #endif //HAVE_LIBREADLINE
25 #ifdef HAVE_LIBCURSES
26 #include <curses.h>
27 //stupid curses macros.
28 #undef erase
29 #undef clear
30 #endif
31 #include "misc.h"
32
33 /* Field Length offsets
34 * Tag 3 0-2
35 * Songname 30 3-32
36 * Artist 30 33-62
37 * Album 30 63-92
38 * Year 4 93-96
39 * Comment 30 97-126
40 * or{
41 * Comment 28 97-124
42 * zero 1 125
43 * Tracknum 1 126
44 * }
45 * Genre 1 127
46 */
47 struct s_v11sub {
48 char comment[28];
49 unsigned char empty;
50 unsigned char tracknum;
51 };
52 struct s_id3{
53 char tag[3];
54 char songname[30];
55 char artist[30];
56 char album[30];
57 char year[4];
58 union{
59 char comment[30];
60 s_v11sub v11;
61 };
62 //char comment[28];
63 //unsigned char empty;
64 //unsigned char tracknum;
65 unsigned char genre;
66 }id3;
67
68 #define NUM_GENRE 148
69
70 #define QUIET_PROGRESS (1 << 0) /* 'File ...' output */
71 #define QUIET_SONG (1 << 1) /* s */
72 #define QUIET_ARTIST (1 << 2) /* n */
73 #define QUIET_ALBUM (1 << 3) /* a */
74 #define QUIET_YEAR (1 << 4) /* y */
75 #define QUIET_COMMENT (1 << 5) /* c */
76 #define QUIET_GENRE (1 << 6) /* g */
77 #define QUIET_TRACK (1 << 7) /* k */
78
79
80 const char *genre_list[NUM_GENRE+1]={
81 "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
82 "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
83 "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
84 "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
85 "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
86 "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
87 "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
88 "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
89 "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
90 "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
91 "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
92 "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
93 "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
94 "Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
95 "Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
96 "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
97 "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
98 "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
99 "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
100 "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
101 "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
102 "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
103 "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
104 "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
105 "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
106 "SynthPop",
107 NULL
108 };
109
110 void i3info(const char *file,int quiet);
111 void i3remove(const char *file,int quiet);
112 void i3rename(const char *file,int quiet,int test);
113 void i3edit(const char * file, const char * defsongname,
114 const char * defartist, const char * defalbum,
115 const char * defyear, const char * defcomment,
116 const char * deftracknum, const char * defgenre,int quiet);
117 void stredit(const char * name, int maxlen, char * buf);
118 void print_genre_list(int mode);
119
version()120 void version()
121 {
122 printf("id3ed v1.10.4 - mpeg layer 3 file information editor\n");
123 }
124
license()125 void license()
126 {
127 printf("Copyright 1998-2001,2003 Matthew Mueller <donut@azstarnet.com>\n");
128 printf("\n\
129 This program is free software; you can redistribute it and/or modify\n\
130 it under the terms of the GNU General Public License as published by\n\
131 the Free Software Foundation; either version 2 of the License, or\n\
132 (at your option) any later version.\n\n\
133 This program is distributed in the hope that it will be useful,\n\
134 but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
135 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\
136 GNU General Public License for more details.\n\n\
137 You should have received a copy of the GNU General Public License\n\
138 along with this program; if not, write to the Free Software\n\
139 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\n");
140 }
141
print_help(void)142 void print_help(void){
143 version();
144 printf("\
145 Usage: id3ed [-s songname] [-n artist] [-a album] [-y year] [-c comment]\n\
146 [-k tracknum] [-g genre] [-q] [-SNAYCKG] [-l/-L] [-r]\n\
147 [-i] <mp3files> [-v]\n\n\
148 -q no line interface; only set tags specified on command\n\
149 line. Use twice to suppress all output except errors.\n\
150 -SNAYCKG prompt to edit the specified tags only\n\
151 Other tags can still be set with the -[snaycg] options.\n\
152 -l/-L display list of genres\n\
153 -r remove id3 tag from files\n\
154 -i show current id3 tag only, don't edit\n\
155 -v output program version/license\n");
156 }
157
main(int argc,char ** argv)158 int main(int argc,char ** argv){
159 int c,quiet=0,alreadyquiet=0,mode=0;
160 char *songname = NULL, *artist = NULL, *album = NULL, *year = NULL,
161 *comment = NULL, *tracknum = NULL, *genre = NULL;
162
163 if (argc<2){
164 print_help();
165 }
166 else {
167 while (EOF != (c = getopt(argc, argv, "s:n:a:y:c:k:g:qSNAYCKGlLrimMv"))){
168 switch (c){
169 case 's':
170 songname = optarg;
171 break;
172 case 'n':
173 artist = optarg;
174 break;
175 case 'a':
176 album = optarg;
177 break;
178 case 'y':
179 year = optarg;
180 break;
181 case 'c':
182 comment = optarg;
183 break;
184 case 'g':
185 genre = optarg;
186 break;
187 case 'k':
188 tracknum = optarg;
189 break;
190 case 'q':
191 #define REALQUIET() {if (!alreadyquiet) { quiet = 0xffff & ~QUIET_PROGRESS; alreadyquiet=1;}}
192 #define UNQUIET(b) { REALQUIET(); quiet &= ~b;}
193 if(alreadyquiet)
194 quiet |= QUIET_PROGRESS;
195 else {
196 REALQUIET();
197 }
198 break;
199 case 'S':
200 UNQUIET(QUIET_SONG);
201 break;
202 case 'N':
203 UNQUIET(QUIET_ARTIST);
204 break;
205 case 'A':
206 UNQUIET(QUIET_ALBUM);
207 break;
208 case 'Y':
209 UNQUIET(QUIET_YEAR);
210 break;
211 case 'C':
212 UNQUIET(QUIET_COMMENT);
213 break;
214 case 'G':
215 UNQUIET(QUIET_GENRE);
216 break;
217 case 'K':
218 UNQUIET(QUIET_TRACK);
219 break;
220 case 'l':
221 print_genre_list(0);
222 break;
223 case 'L':
224 print_genre_list(1);
225 break;
226 case 'r':
227 mode=1;
228 break;
229 case 'i':
230 mode=2;
231 break;
232 case 'v':
233 version();
234 license();
235 break;
236 case ':':
237 case '?':
238 //fprintf(stderr, "Illegal parameters\n");
239 print_help();
240 return 1;
241 case 'm':
242 mode=3;
243 break;
244 case 'M':
245 mode=4;
246 break;
247 }
248 }
249 //ntf("quiet=%i alreadyquiet=%i\n",quiet,alreadyquiet);
250 for (int i=optind;i<argc;i++)
251 switch (mode){
252 case 3:
253 case 4:
254 i3rename(argv[i],quiet,mode==3);
255 break;
256 case 2:
257 i3info(argv[i],quiet);
258 break;
259 case 1:
260 i3remove(argv[i],quiet);
261 break;
262 default:
263 i3edit(argv[i],songname,artist,album,year,comment,tracknum,genre,quiet);
264 }
265 }
266 return 0;
267 }
268
print_genre_list(int mode)269 void print_genre_list(int mode){
270 int c=0,w=0;
271 #ifdef HAVE_LIBCURSES
272 c=COLS;
273 #endif
274 if (c<=0)c=80;
275 w=(c/26);
276 if (w<1)w=1;
277 // printf("c=%i w=%i\n",c,w);
278 //for (int x=0, y=0, i=0;genre_list[i];mode?i++:(i=y+x*((NUM_GENRE+w-1)/w))){
279 for (int x=0, y=0, i=0;genre_list[i];mode?i++:(i=y+x*((NUM_GENRE)/w))){
280 printf("%3i: %-20s",i,genre_list[i]);
281 // printf("x%i,y%i,i%i\n",x,y,i);
282 if (x>=w-1){
283 x=0;y++;
284 }else{
285 x++;
286 }
287 if (x==0){
288 if (i==NUM_GENRE-1)
289 break;
290 printf("\n");
291 }
292 else
293 printf(" ");
294 }
295 printf("\n");
296 }
297
agenretoi(const char * g)298 int agenretoi(const char * g){
299 int cmpl=g?strlen(g):0;
300 while (cmpl>0 && isspace(g[cmpl-1]))
301 --cmpl;//ignore trailing spaces
302 if (cmpl>0){
303 int i;
304 char * errpos;
305 for(i=0;i<NUM_GENRE;i++)
306 if(strncasecmp(genre_list[i],g,cmpl)==0)
307 return i;
308 i=strtol(g,&errpos,0);
309 if (!*errpos)
310 return i;
311 }
312 printf("'%s' is not a valid genre name, nor a number between 0 and 255\n",g);
313 return -1;
314 }
315
316 #ifdef HAVE_LIBREADLINE
317 char *my_rl_default;
318
set_rl_default(void)319 int set_rl_default(void){
320 rl_insert_text(my_rl_default);
321 //rl_forced_update_display();
322 return 0;
323 }
324 #ifdef HAVE_SET_H
325 #include <set.h>
operator ()ltstr326 struct ltstr{ bool operator()(const char* s1, const char* s2) const {return strcasecmp(s1, s2) < 0;} };
genre_generator(const char * text,int state)327 const char *genre_generator(const char *text,int state){
328 static set<const char*, ltstr> matches;
329 static set<const char*, ltstr>::iterator curm=matches.end();
330 const char *ret;
331 if (state==0){
332 int i;
333 int l=strlen(text);
334 matches.erase(matches.begin(),matches.end());
335 for (i=0;i<NUM_GENRE;i++)
336 if (strncasecmp(genre_list[i],text,l)==0)
337 matches.insert(genre_list[i]);
338 curm=matches.begin();
339 }
340 if (curm==matches.end())return NULL;
341 ret=(*curm);
342 ++curm;
343 return strdup(ret);//return must be malloc()'d
344 }
345 #endif //HAVE_SET_H
346 #endif //HAVE_LIBREADLINE
genreedit(unsigned char & d)347 void genreedit(unsigned char &d){
348 char def[40];
349 int g=-1;
350 if (d<NUM_GENRE)
351 strcpy(def,genre_list[d]);
352 else
353 sprintf(def,"%i",d);
354 #ifdef HAVE_LIBREADLINE
355 char *str=NULL;
356 my_rl_default=def;
357 rl_startup_hook=(Function*)set_rl_default;
358 #ifdef HAVE_SET_H
359 (const char *(*)(const char *,int))rl_completion_entry_function=genre_generator;
360 #endif
361 // rl_attempted_completion_function = (CPPFunction *)fileman_completion;
362 #else
363 char str[40];
364 #endif
365
366 do {
367 #ifdef HAVE_LIBREADLINE
368 if (str){strcpy(def,str);free(str);}
369 if((str=readline("genre[0-255/name]: "))){
370 if (*str){
371 add_history(str);
372 }
373 }
374 #else
375 printf("genre[0-255/name def:%s]: ",def);
376 fflush(stdout);
377 if (fgets(str,40,stdin)){
378 char *t;
379 if ((t=strchr(str,'\n')))
380 t[0]=0;
381 }
382 if(!*str) {
383 // Blank; accept default.
384 g = d;
385 break;
386 }
387
388 #endif
389 if(str) g = agenretoi(str);
390 } while (g == -1);
391
392 #ifdef HAVE_LIBREADLINE
393 free(str);
394 rl_completion_entry_function=NULL;//use normal completer again.
395 #endif
396 d=g;
397 }
stredit(const char * name,int maxlen,char * buf)398 void stredit(const char * name, int maxlen, char * buf){
399 char def[40];
400 strncpy(def,buf,maxlen);
401 def[maxlen]=0;
402 #ifdef HAVE_LIBREADLINE
403 char prompt[40];
404 char *str;
405 sprintf(prompt,"%s[max:%i]: ",name,maxlen);
406 my_rl_default=def;
407 rl_startup_hook=(Function*)set_rl_default;
408 if((str=readline(prompt))){
409 strncpy(buf,str,maxlen);
410 if (*str){
411 add_history(str);
412 }
413 free(str);
414 }
415 #else
416 char str[40];
417 printf("%s[max:%i def:%s]: ",name,maxlen,def);
418 fflush(stdout);
419 if (fgets(str,40,stdin)){
420 char *t;
421 if ((t=strchr(str,'\n')))
422 t[0]=0;
423 if (str[0])
424 strncpy(buf,str,maxlen);
425 }else if (ferror(stdin))
426 perror("fgets");
427 else
428 strncpy(buf,"",maxlen);
429 #endif
430 }
431
432 //##### TODO: make renaming configurable?
i3rename(const char * file,int quiet,int test)433 void i3rename(const char *file,int quiet,int test){
434 int f;
435 if (doopen(f,file,RDMODE))return;
436 if ((!quiet) || test){
437 printf("%s->",file);fflush(stdout);
438 }
439 off_t end=lseek(f,0,SEEK_END);
440 if (end>=0 && end<128) {
441 close (f);
442 if ((!quiet))
443 printf("(no tag)\n");
444 return;
445 }
446 if (lseek(f,-128,SEEK_END)<0){
447 close (f);
448 perror("lseek");return;
449 }
450 if (doread(f,&id3,sizeof(id3),"id3buf"))return;
451 close (f);
452 if (!strncmp(id3.tag,"TAG",3)){
453 char newname[256];
454 int track=0;
455 if (id3.v11.empty==0)
456 track=id3.v11.tracknum;
457 sprintf(newname,"%02i-%.*s.mp3",track,(int)sizeof(id3.songname),id3.songname);//##### TODO: handle directories in file, handle invalid chars in songname
458 if ((!quiet) || test){
459 printf("%s\n",newname);
460 }
461 if (!test){
462 if (rename(file,newname))
463 perror("rename");return;
464 }
465 }else
466 if ((!quiet))
467 printf("(no tag)\n");
468 }
469
i3info(const char * file,int quiet)470 void i3info(const char *file,int quiet){
471 int f;
472 if (doopen(f,file,RDMODE))return;
473 printf("%s: ",file);fflush(stdout);
474 off_t end=lseek(f,0,SEEK_END);
475 if (end>=0 && end<128) {
476 close (f);
477 if ((!quiet))
478 printf("(no tag)\n");
479 return;
480 }
481 if (lseek(f,-128,SEEK_END)<0){
482 close (f);
483 perror("lseek");return;
484 }
485 if (doread(f,&id3,sizeof(id3),"id3buf"))return;
486 if (!strncmp(id3.tag,"TAG",3)){
487 if (id3.v11.empty==0)
488 printf("(tag v1.1%s)\n",id3.v11.tracknum?"":"?");
489 else
490 printf("(tag v1.0)\n");
491 #define PRINTINFO(f) printf("%s: %.*s\n",#f, (int)sizeof(id3.f), id3.f)
492 PRINTINFO(songname);
493 PRINTINFO(artist);
494 PRINTINFO(album);
495 PRINTINFO(year);
496 PRINTINFO(comment);
497 if (id3.v11.empty==0)
498 printf("tracknum: %i\n", id3.v11.tracknum);
499 printf("genre: %s(%i)\n\n",(id3.genre<NUM_GENRE)?genre_list[id3.genre]:"unknown",id3.genre);
500
501 }else
502 if ((!quiet))
503 printf("(no tag)\n");
504
505 close (f);
506 }
507
i3remove(const char * file,int quiet)508 void i3remove(const char *file,int quiet){
509 int f;
510 if (doopen(f,file,RDWRMODE))return;
511 off_t end=lseek(f,0,SEEK_END);
512 if (end>=0 && end<128) {
513 close (f);
514 if ((!quiet))
515 printf("no tag in %s\n",file);
516 return;
517 }
518 if ((end=lseek(f,-128,SEEK_END))<0){
519 close (f);
520 perror("lseek");return;
521 }
522 if (doread(f,&id3,sizeof(id3),"id3buf"))return;
523 if (!strncmp(id3.tag,"TAG",3)){
524 ftruncate(f,end);
525 if ((!quiet))
526 printf("tag removed from %s\n",file);
527 }else
528 if ((!quiet))
529 printf("no tag in %s\n",file);
530
531 close (f);
532 }
533
i3edit(const char * file,const char * defsongname,const char * defartist,const char * defalbum,const char * defyear,const char * defcomment,const char * deftracknum,const char * defgenre,int quiet)534 void i3edit(const char * file, const char * defsongname,
535 const char * defartist, const char * defalbum,
536 const char * defyear, const char * defcomment,
537 const char * deftracknum, const char * defgenre, int quiet){
538 int f,hasid3,commentsize=28;
539 if (!(quiet&QUIET_PROGRESS))
540 printf("\nFile %s: ", file);
541 if (doopen(f,file,RDWRMODE))return;
542 off_t end=lseek(f,0,SEEK_END);
543 if (end>=0 && end<128) {
544 memset(&id3,0,sizeof(id3));
545 id3.tag[0]='T';id3.tag[1]='A';id3.tag[2]='G';
546 id3.genre=255;
547 hasid3=0;
548 if (!(quiet&QUIET_PROGRESS))
549 printf("(no tag)\n");
550 } else {
551 if (lseek(f,-128,SEEK_END)<0){
552 close(f);
553 perror("lseek");return;
554 }
555 if (doread(f,&id3,sizeof(id3),"id3buf"))return;
556 if (strncmp(id3.tag,"TAG",3)){
557 memset(&id3,0,sizeof(id3));
558 id3.tag[0]='T';id3.tag[1]='A';id3.tag[2]='G';
559 id3.genre=255;
560 hasid3=0;
561 if (!(quiet&QUIET_PROGRESS))
562 printf("(no tag)\n");
563 } else {
564 if (id3.v11.empty==0){
565 if (!(quiet&QUIET_PROGRESS))
566 printf("(tag v1.1)\n");
567 hasid3=2;//id3 v1.1
568 }else{
569 if (!(quiet&1))
570 printf("(tag v1.0");
571 if (deftracknum){
572 hasid3=2;
573 if (!(quiet&QUIET_PROGRESS))
574 printf("->1.1)\n");
575 }else{
576 hasid3=1;//id3 v1.0
577 commentsize=30;
578 if (!(quiet&QUIET_PROGRESS))
579 printf(")\n");
580 }
581 }
582 }
583 }
584 if (defsongname){
585 memset(id3.songname,0,sizeof(id3.songname));
586 strncpy(id3.songname,defsongname,sizeof(id3.songname));
587 }
588 if (defartist){
589 memset(id3.artist,0,sizeof(id3.artist));
590 strncpy(id3.artist,defartist,sizeof(id3.artist));
591 }
592 if (defalbum){
593 memset(id3.album,0,sizeof(id3.album));
594 strncpy(id3.album,defalbum,sizeof(id3.album));
595 }
596 if (defyear){
597 memset(id3.year,0,sizeof(id3.year));
598 strncpy(id3.year,defyear,sizeof(id3.year));
599 }
600 if (defcomment){
601 memset(id3.comment,0,commentsize);
602 strncpy(id3.comment,defcomment,commentsize);
603 }
604 if (deftracknum){
605 //memset(id3.tracknum,0,sizeof(id3.tracknum));
606 id3.v11.empty=0;
607 id3.v11.tracknum = atoi(deftracknum);
608 //strncpy(id3.tracknum,deftracknum,sizeof(id3.tracknum));
609 }
610 if (defgenre){
611 id3.genre = agenretoi(defgenre);
612 //unsigned tmp;
613 //sscanf(defgenre,"%u",&tmp);
614 //id3.genre = tmp;
615 }
616 if (!(quiet&QUIET_SONG))
617 stredit("songname",30,id3.songname);
618 if (!(quiet&QUIET_ARTIST))
619 stredit("artist",30,id3.artist);
620 if (!(quiet&QUIET_ALBUM))
621 stredit("album",30,id3.album);
622 if (!(quiet&QUIET_YEAR))
623 stredit("year",4,id3.year);
624 if (!(quiet&QUIET_COMMENT))
625 stredit("comment",commentsize,id3.comment);
626 if (hasid3!=1 && !(quiet&QUIET_TRACK)) {
627 char tbuf[3];
628 sprintf(tbuf, "%i", id3.v11.tracknum);
629 stredit("tracknum", 3, tbuf);
630 id3.v11.empty=0;
631 id3.v11.tracknum=atoi(tbuf);
632 }
633 if (!(quiet&QUIET_GENRE)){
634 //char sbuf[4];
635 //sprintf(sbuf,"%i",id3.genre);
636 //stredit("genre",3,sbuf);
637 //id3.genre=atoi(sbuf);
638 genreedit(id3.genre);
639 }
640 if (hasid3){
641 if (lseek(f,-128,SEEK_END)<0){
642 perror("lseek");return;
643 }
644 }else{
645 if (lseek(f,0,SEEK_END)<0){
646 perror("lseek");return;
647 }
648 }
649 if (dowrite(f,&id3,sizeof(id3),"id3buf"))return;
650 close(f);
651 }
652