vcomment.c 13 KB
Newer Older
Ralph Giles's avatar
 
Ralph Giles committed
1 2
/* This program is licensed under the GNU General Public License, 
 * version 2, a copy of which is included with this program.
Michael Smith's avatar
 
Michael Smith committed
3
 *
4
 * (c) 2000-2002 Michael Smith <msmith@xiph.org>
Ralph Giles's avatar
Ralph Giles committed
5
 * (c) 2001 Ralph Giles <giles@xiph.org>
Michael Smith's avatar
 
Michael Smith committed
6
 *
Ralph Giles's avatar
 
Ralph Giles committed
7
 * Front end to show how to use vcedit;
Michael Smith's avatar
 
Michael Smith committed
8
 * Of limited usability on its own, but could be useful.
Michael Smith's avatar
 
Michael Smith committed
9 10
 */

11 12 13
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
Ralph Giles's avatar
 
Ralph Giles committed
14

Michael Smith's avatar
 
Michael Smith committed
15
#include <stdio.h>
Ralph Giles's avatar
 
Ralph Giles committed
16
#include <string.h>
Michael Smith's avatar
 
Michael Smith committed
17
#include <stdlib.h>
18
#include <locale.h>
19

20
#include "getopt.h"
21
#include "utf8.h"
22
#include "i18n.h"
Ralph Giles's avatar
 
Ralph Giles committed
23

Michael Smith's avatar
 
Michael Smith committed
24 25
#include "vcedit.h"

26

Ralph Giles's avatar
 
Ralph Giles committed
27 28 29 30 31 32
/* getopt format struct */
struct option long_options[] = {
	{"list",0,0,'l'},
	{"write",0,0,'w'},
	{"help",0,0,'h'},
	{"quiet",0,0,'q'},
33
    {"version", 0, 0, 'V'},
Ralph Giles's avatar
 
Ralph Giles committed
34
	{"commentfile",1,0,'c'},
35
    {"raw", 0,0,'R'},
Ralph Giles's avatar
 
Ralph Giles committed
36 37 38 39 40 41 42 43 44
	{NULL,0,0,0}
};

/* local parameter storage from parsed options */
typedef struct {
	int	mode;
	char	*infilename, *outfilename;
	char	*commentfilename;
	FILE	*in, *out, *com;
45 46
	int commentcount;
	char **comments;
47
	int tempoutfile;
48
    int raw;
Ralph Giles's avatar
 
Ralph Giles committed
49 50 51 52 53
} param_t;

#define MODE_NONE  0
#define MODE_LIST  1
#define MODE_WRITE 2
54
#define MODE_APPEND 3
Ralph Giles's avatar
 
Ralph Giles committed
55 56 57

/* prototypes */
void usage(void);
58 59
void print_comments(FILE *out, vorbis_comment *vc, int raw);
int  add_comment(char *line, vorbis_comment *vc, int raw);
Ralph Giles's avatar
 
Ralph Giles committed
60 61

param_t	*new_param(void);
Michael Smith's avatar
Michael Smith committed
62
void free_param(param_t *param);
Ralph Giles's avatar
 
Ralph Giles committed
63 64
void parse_options(int argc, char *argv[], param_t *param);
void open_files(param_t *p);
65
void close_files(param_t *p, int output_written);
Ralph Giles's avatar
 
Ralph Giles committed
66 67 68 69 70 71 72 73 74 75 76 77 78 79


/**********
   main.c

   This is the main function where options are read and written
   you should be able to just read this function and see how
   to call the vcedit routines. Details of how to pack/unpack the
   vorbis_comment structure itself are in the following two routines.
   The rest of the file is ui dressing so make the program minimally
   useful as a command line utility and can generally be ignored.

***********/

Michael Smith's avatar
 
Michael Smith committed
80 81 82
int main(int argc, char **argv)
{
	vcedit_state *state;
Ralph Giles's avatar
 
Ralph Giles committed
83 84
	vorbis_comment *vc;
	param_t	*param;
85
	int i;
Michael Smith's avatar
 
Michael Smith committed
86

87 88 89 90
	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);

Ralph Giles's avatar
 
Ralph Giles committed
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
	/* initialize the cmdline interface */
	param = new_param();
	parse_options(argc, argv, param);

	/* take care of opening the requested files */
	/* relevent file pointers are returned in the param struct */
	open_files(param);

	/* which mode are we in? */

	if (param->mode == MODE_LIST) {
		
		state = vcedit_new_state();

		if(vcedit_open(state, param->in) < 0)
		{
107
			fprintf(stderr, _("Failed to open file as vorbis: %s\n"), 
108
					vcedit_error(state));
109
            close_files(param, 0);
Michael Smith's avatar
Michael Smith committed
110
            free_param(param);
111
            vcedit_clear(state);
Ralph Giles's avatar
 
Ralph Giles committed
112 113 114 115 116
			return 1;
		}

		/* extract and display the comments */
		vc = vcedit_comments(state);
117
		print_comments(param->com, vc, param->raw);
Ralph Giles's avatar
 
Ralph Giles committed
118 119 120 121

		/* done */
		vcedit_clear(state);

122
		close_files(param, 0);
Michael Smith's avatar
Michael Smith committed
123
        free_param(param);
Ralph Giles's avatar
 
Ralph Giles committed
124
		return 0;		
Michael Smith's avatar
 
Michael Smith committed
125 126
	}

127
	if (param->mode == MODE_WRITE || param->mode == MODE_APPEND) {
Ralph Giles's avatar
 
Ralph Giles committed
128 129 130 131 132

		state = vcedit_new_state();

		if(vcedit_open(state, param->in) < 0)
		{
133
			fprintf(stderr, _("Failed to open file as vorbis: %s\n"), 
134
					vcedit_error(state));
135
            close_files(param, 0);
Michael Smith's avatar
Michael Smith committed
136
            free_param(param);
137
            vcedit_clear(state);
Ralph Giles's avatar
 
Ralph Giles committed
138 139 140 141 142
			return 1;
		}

		/* grab and clear the exisiting comments */
		vc = vcedit_comments(state);
143 144 145 146 147
		if(param->mode != MODE_APPEND) 
		{
			vorbis_comment_clear(vc);
			vorbis_comment_init(vc);
		}
Ralph Giles's avatar
 
Ralph Giles committed
148

149 150
		for(i=0; i < param->commentcount; i++)
		{
151
			if(add_comment(param->comments[i], vc, param->raw) < 0)
152
				fprintf(stderr, _("Bad comment: \"%s\"\n"), param->comments[i]);
153 154
		}

Ralph Giles's avatar
 
Ralph Giles committed
155
		/* build the replacement structure */
156
		if(param->commentcount==0)
Ralph Giles's avatar
 
Ralph Giles committed
157
		{
Ralph Giles's avatar
 
Ralph Giles committed
158
			/* FIXME should use a resizeable buffer! */
Ralph Giles's avatar
 
Ralph Giles committed
159 160 161
			char *buf = (char *)malloc(sizeof(char)*1024);

			while (fgets(buf, 1024, param->com))
162
				if (add_comment(buf, vc, param->raw) < 0) {
Ralph Giles's avatar
 
Ralph Giles committed
163
					fprintf(stderr,
164
						_("bad comment: \"%s\"\n"),
Ralph Giles's avatar
 
Ralph Giles committed
165 166 167
						buf);
				}
			
Ralph Giles's avatar
 
Ralph Giles committed
168 169 170 171 172 173
			free(buf);
		}

		/* write out the modified stream */
		if(vcedit_write(state, param->out) < 0)
		{
174
			fprintf(stderr, _("Failed to write comments to output file: %s\n"), 
175
					vcedit_error(state));
176
            close_files(param, 0);
Michael Smith's avatar
Michael Smith committed
177
            free_param(param);
178
            vcedit_clear(state);
Ralph Giles's avatar
 
Ralph Giles committed
179 180 181 182 183 184
			return 1;
		}

		/* done */
		vcedit_clear(state);
		
185
		close_files(param, 1);
Michael Smith's avatar
Michael Smith committed
186
        free_param(param);
Ralph Giles's avatar
 
Ralph Giles committed
187
		return 0;
Michael Smith's avatar
 
Michael Smith committed
188 189
	}

Ralph Giles's avatar
 
Ralph Giles committed
190
	/* should never reach this point */
191
	fprintf(stderr, _("no action specified\n"));
Michael Smith's avatar
Michael Smith committed
192
    free_param(param);
Ralph Giles's avatar
 
Ralph Giles committed
193 194 195 196 197 198 199 200 201 202 203 204
	return 1;
}

/**********

   Print out the comments from the vorbis structure

   this version just dumps the raw strings
   a more elegant version would use vorbis_comment_query()

***********/

205
void print_comments(FILE *out, vorbis_comment *vc, int raw)
Ralph Giles's avatar
 
Ralph Giles committed
206 207
{
	int i;
208
    char *decoded_value;
Ralph Giles's avatar
 
Ralph Giles committed
209 210

	for (i = 0; i < vc->comments; i++)
211
    {
212
	    if (!raw && utf8_decode(vc->user_comments[i], &decoded_value) >= 0)
213 214 215 216 217 218 219
        {
    		fprintf(out, "%s\n", decoded_value);
            free(decoded_value);
        }
        else
            fprintf(out, "%s\n", vc->user_comments[i]);
    }
Ralph Giles's avatar
 
Ralph Giles committed
220 221 222 223
}

/**********

224
   Take a line of the form "TAG=value string", parse it, convert the
225
   value to UTF-8, and add it to the
226
   vorbis_comment structure. Error checking is performed.
Ralph Giles's avatar
 
Ralph Giles committed
227 228 229 230 231 232

   Note that this assumes a null-terminated string, which may cause
   problems with > 8-bit character sets!

***********/

233
int  add_comment(char *line, vorbis_comment *vc, int raw)
Ralph Giles's avatar
 
Ralph Giles committed
234
{
235
	char	*mark, *value, *utf8_value;
Ralph Giles's avatar
 
Ralph Giles committed
236 237

	/* strip any terminal newline */
Michael Smith's avatar
 
Michael Smith committed
238
	{
Ralph Giles's avatar
 
Ralph Giles committed
239 240
		int len = strlen(line);
		if (line[len-1] == '\n') line[len-1] = '\0';
Michael Smith's avatar
 
Michael Smith committed
241 242
	}

Ralph Giles's avatar
 
Ralph Giles committed
243
	/* validation: basically, we assume it's a tag
Michael Smith's avatar
 
Michael Smith committed
244 245 246
	 * if it has an '=' after one or more valid characters,
	 * as the comment spec requires. For the moment, we
	 * also restrict ourselves to 0-terminated values */
Michael Smith's avatar
 
Michael Smith committed
247

248
	mark = strchr(line, '=');
Ralph Giles's avatar
 
Ralph Giles committed
249 250 251 252
	if (mark == NULL) return -1;

	value = line;
	while (value < mark) {
Michael Smith's avatar
 
Michael Smith committed
253 254
		if(*value < 0x20 || *value > 0x7d || *value == 0x3d) return -1;
		value++;
Michael Smith's avatar
 
Michael Smith committed
255 256
	}

Ralph Giles's avatar
 
Ralph Giles committed
257 258 259
	/* split the line by turning the '=' in to a null */
	*mark = '\0';	
	value++;
Michael Smith's avatar
 
Michael Smith committed
260

261 262 263 264
    if(raw) {
        vorbis_comment_add_tag(vc, line, value);
        return 0;
    }
265
	/* convert the value from the native charset to UTF-8 */
266
    else if (utf8_encode(value, &utf8_value) >= 0) {
267 268
		
		/* append the comment and return */
269
		vorbis_comment_add_tag(vc, line, utf8_value);
270
        free(utf8_value);
271 272
		return 0;
	} else {
273
		fprintf(stderr, _("Couldn't convert comment to UTF-8, "
274
			"cannot add\n"));
275 276
		return -1;
	}
Ralph Giles's avatar
 
Ralph Giles committed
277
}
Michael Smith's avatar
 
Michael Smith committed
278 279


Ralph Giles's avatar
 
Ralph Giles committed
280 281 282 283 284 285 286 287 288 289 290
/*** ui-specific routines ***/

/**********

   Print out to usage summary for the cmdline interface (ui)

***********/

void usage(void)
{
	fprintf(stderr, 
291
		_("Usage: \n"
Ralph Giles's avatar
 
Ralph Giles committed
292
		"  vorbiscomment [-l] file.ogg (to list the comments)\n"
293
		"  vorbiscomment -a in.ogg out.ogg (to append comments)\n"
Ralph Giles's avatar
 
Ralph Giles committed
294 295 296 297
		"  vorbiscomment -w in.ogg out.ogg (to modify comments)\n"
		"	in the write case, a new set of comments in the form\n"
		"	'TAG=value' is expected on stdin. This set will\n"
		"	completely replace the existing set.\n"
298 299 300 301 302 303
		"   Either of -a and -w can take only a single filename,\n"
		"   in which case a temporary file will be used.\n"
		"   -c can be used to take comments from a specified file\n"
		"   instead of stdin.\n"
		"   Example: vorbiscomment -a in.ogg -c comments.txt\n"
		"   will append the comments in comments.txt to in.ogg\n"
304 305 306 307
		"   Finally, you may specify any number of tags to add on\n"
		"   the command line using the -t option. e.g.\n"
		"   vorbiscomment -a in.ogg -t \"ARTIST=Some Guy\" -t \"TITLE=A Title\"\n"
		"   (note that when using this, reading comments from the comment\n"
308
		"   file or stdin is disabled)\n"
309
        "   Raw mode (--raw, -R) will read and write comments in UTF-8,\n"
310 311 312 313 314
        "   rather than converting to the user's character set. This is\n"
        "   useful for using vorbiscomment in scripts. However, this is\n"
        "   not sufficient for general round-tripping of comments in all\n"
        "   cases.\n")
        
Ralph Giles's avatar
 
Ralph Giles committed
315 316 317
	); 
}

Michael Smith's avatar
Michael Smith committed
318 319 320 321 322
void free_param(param_t *param) {
    free(param->infilename);
    free(param->outfilename);
    free(param);
}
Ralph Giles's avatar
 
Ralph Giles committed
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345

/**********

   allocate and initialize a the parameter struct

***********/

param_t *new_param(void)
{
	param_t *param = (param_t *)malloc(sizeof(param_t));

	/* mode */
	param->mode = MODE_LIST;

	/* filenames */
	param->infilename  = NULL;
	param->outfilename = NULL;
	param->commentfilename = "-";	/* default */

	/* file pointers */
	param->in = param->out = NULL;
	param->com = NULL;

346 347 348 349
	param->commentcount=0;
	param->comments=NULL;
	param->tempoutfile=0;

350 351
    param->raw = 0;

Ralph Giles's avatar
 
Ralph Giles committed
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
	return param;
}

/**********
   parse_options()

   This function takes care of parsing the command line options
   with getopt() and fills out the param struct with the mode,
   flags, and filenames.

***********/

void parse_options(int argc, char *argv[], param_t *param)
{
	int ret;
	int option_index = 1;

369 370
	setlocale(LC_ALL, "");

371
	while ((ret = getopt_long(argc, argv, "alwhqVc:t:R",
Ralph Giles's avatar
 
Ralph Giles committed
372 373 374
			long_options, &option_index)) != -1) {
		switch (ret) {
			case 0:
375
				fprintf(stderr, _("Internal error parsing command options\n"));
Ralph Giles's avatar
 
Ralph Giles committed
376 377 378 379 380
				exit(1);
				break;
			case 'l':
				param->mode = MODE_LIST;
				break;
381 382
            case 'R':
                param->raw = 1;
383
                break;
Ralph Giles's avatar
 
Ralph Giles committed
384 385 386
			case 'w':
				param->mode = MODE_WRITE;
				break;
387 388 389
			case 'a':
				param->mode = MODE_APPEND;
				break;
390 391 392 393
            case 'V':
                fprintf(stderr, "Vorbiscomment " VERSION "\n");
                exit(0);
                break;
Ralph Giles's avatar
 
Ralph Giles committed
394 395 396 397 398 399 400 401 402 403
			case 'h':
				usage();
				exit(0);
				break;
			case 'q':
				/* set quiet flag */
				break;
			case 'c':
				param->commentfilename = strdup(optarg);
				break;
404 405 406 407 408
			case 't':
				param->comments = realloc(param->comments, 
						(param->commentcount+1)*sizeof(char *));
				param->comments[param->commentcount++] = strdup(optarg);
				break;
Ralph Giles's avatar
 
Ralph Giles committed
409 410 411 412
			default:
				usage();
				exit(1);
		}
Michael Smith's avatar
 
Michael Smith committed
413 414
	}

Ralph Giles's avatar
 
Ralph Giles committed
415
	/* remaining bits must be the filenames */
416 417 418
	if((param->mode == MODE_LIST && (argc-optind) != 1) ||
	   ((param->mode == MODE_WRITE || param->mode == MODE_APPEND) &&
	   ((argc-optind) < 1 || (argc-optind) > 2))) {
Ralph Giles's avatar
 
Ralph Giles committed
419 420 421
			usage();
			exit(1);
	}
422

Ralph Giles's avatar
 
Ralph Giles committed
423
	param->infilename = strdup(argv[optind]);
424 425 426 427 428 429 430 431 432 433 434 435
	if (param->mode == MODE_WRITE || param->mode == MODE_APPEND)
	{
		if(argc-optind == 1)
		{
			param->tempoutfile = 1;
			param->outfilename = malloc(strlen(param->infilename)+8);
			strcpy(param->outfilename, param->infilename);
			strcat(param->outfilename, ".vctemp");
		}
		else
			param->outfilename = strdup(argv[optind+1]);
	}
Ralph Giles's avatar
 
Ralph Giles committed
436
}
Michael Smith's avatar
 
Michael Smith committed
437

Ralph Giles's avatar
 
Ralph Giles committed
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
/**********
   open_files()

   This function takes care of opening the appropriate files
   based on the mode and filenames in the param structure.
   A filename of '-' is interpreted as stdin/out.

   The idea is just to hide the tedious checking so main()
   is easier to follow as an example.

***********/

void open_files(param_t *p)
{
	/* for all modes, open the input file */

	if (strncmp(p->infilename,"-",2) == 0) {
		p->in = stdin;
	} else {
		p->in = fopen(p->infilename, "rb");
	}
	if (p->in == NULL) {
		fprintf(stderr,
461
			_("Error opening input file '%s'.\n"),
Ralph Giles's avatar
 
Ralph Giles committed
462 463 464 465
			p->infilename);
		exit(1);
	}

466
	if (p->mode == MODE_WRITE || p->mode == MODE_APPEND) { 
Ralph Giles's avatar
 
Ralph Giles committed
467 468

		/* open output for write mode */
469 470 471 472
        if(!strcmp(p->infilename, p->outfilename)) {
            fprintf(stderr, _("Input filename may not be the same as output filename\n"));
            exit(1);
        }
Ralph Giles's avatar
 
Ralph Giles committed
473 474 475 476 477 478 479 480

		if (strncmp(p->outfilename,"-",2) == 0) {
			p->out = stdout;
		} else {
			p->out = fopen(p->outfilename, "wb");
		}
		if(p->out == NULL) {
			fprintf(stderr,
481
				_("Error opening output file '%s'.\n"),
Ralph Giles's avatar
 
Ralph Giles committed
482 483 484 485 486 487 488 489 490 491
				p->outfilename);
			exit(1);
		}

		/* commentfile is input */
		
		if ((p->commentfilename == NULL) ||
				(strncmp(p->commentfilename,"-",2) == 0)) {
			p->com = stdin;
		} else {
492
			p->com = fopen(p->commentfilename, "r");
Ralph Giles's avatar
 
Ralph Giles committed
493 494 495
		}
		if (p->com == NULL) {
			fprintf(stderr,
496
				_("Error opening comment file '%s'.\n"),
Ralph Giles's avatar
 
Ralph Giles committed
497 498 499 500 501 502 503 504 505 506 507 508
				p->commentfilename);
			exit(1);
		}

	} else {

		/* in list mode, commentfile is output */

		if ((p->commentfilename == NULL) ||
				(strncmp(p->commentfilename,"-",2) == 0)) {
			p->com = stdout;
		} else {
509
			p->com = fopen(p->commentfilename, "w");
Ralph Giles's avatar
 
Ralph Giles committed
510 511 512
		}
		if (p->com == NULL) {
			fprintf(stderr,
513
				_("Error opening comment file '%s'\n"),
Ralph Giles's avatar
 
Ralph Giles committed
514 515 516 517 518 519
				p->commentfilename);
			exit(1);
		}
	}

	/* all done */
Michael Smith's avatar
 
Michael Smith committed
520 521
}

Ralph Giles's avatar
 
Ralph Giles committed
522 523 524 525 526 527 528
/**********
   close_files()

   Do some quick clean-up.

***********/

529
void close_files(param_t *p, int output_written)
Ralph Giles's avatar
 
Ralph Giles committed
530
{
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
  if (p->in != NULL && p->in != stdin) fclose(p->in);
  if (p->out != NULL && p->out != stdout) fclose(p->out);
  if (p->com != NULL && p->com != stdout && p->com != stdin) fclose(p->com);

  if(p->tempoutfile) {
    if(output_written) {
      /* Some platforms fail to rename a file if the new name already 
       * exists, so we need to remove, then rename. How stupid.
       */
      if(rename(p->outfilename, p->infilename)) {
        if(remove(p->infilename))
          fprintf(stderr, _("Error removing old file %s\n"), p->infilename);
        else if(rename(p->outfilename, p->infilename)) 
          fprintf(stderr, _("Error renaming %s to %s\n"), p->outfilename, 
                  p->infilename);
      }
    }
    else {
      if(remove(p->outfilename)) {
        fprintf(stderr, _("Error removing erroneous temporary file %s\n"), 
                    p->outfilename);
      }
553
    }
554
  }
Ralph Giles's avatar
 
Ralph Giles committed
555
}
556