#include "opt.h" #include "list.h" #include "xmalloc.h" #include "comment.h" #include "utils.h" #include "file.h" #include "tagger.h" #include "mp3-tagger.h" #include "id3.h" #include "config.h" #if defined(CONFIG_VORBIS) #include "vorbis-tagger.h" #include #endif #include #include #include #include #include #include #include char *program_name; /* current locale's charset */ const char *charset = NULL; /* charset of ID3 tags. usually ISO_8859-1 but can be anything */ const char *default_id3_charset = "ISO_8859-1"; const char *id3_read_charset = NULL; const char *id3_write_charset = NULL; /* if non-zero no changes are made to file and operations are printed instead */ static int flag_pretend = 0; static int flag_capitalize = 0; static int flag_conv_usc = 0; static int flag_uniq = 0; static int flag_clear = 0; static int flag_annoying = 0; static int flag_lowercase = 0; static const char *flag_pattern = NULL; static const char *flag_rename = NULL; static int flag_id3_wcs = 0; static int rename_file_or_pretend(const char *filename, const char *new_filename) { if (flag_pretend) { printf("'%s' => '%s'\n", filename, new_filename); return 0; } return rename_file(filename, new_filename); } static int line_to_key_value(const char *line, char **keyp, char **valuep) { const char *key, *value; key = line; value = line; while (1) { if (*value == '=') break; if (*value == 0) goto err1; if (*value < 0x20 || *value > 0x7d) { fprintf(stderr, "key must be in 7-bit ascii format\n"); return -1; } value++; } if (key == value) goto err1; /* skip '=' */ value++; *keyp = xstrndup(key, value - key - 1); *valuep = xstrdup(value); return 0; err1: fprintf(stderr, "expecting 'key=value'\n"); return -1; } static LIST_HEAD(comment_head); static LIST_HEAD(delete_head); static int is_mp3_header(const unsigned char *data) { unsigned char byte; /* MPEG Layer III files either have an ID3 header or a magic word */ if (strncmp((const char *)data, "ID3", 3) == 0) return 1; byte = data[1] & 0xfe; return data[0] == 0xff && (byte == 0xe2 || byte == 0xf2 || byte == 0xfa); } static int is_mp3(FILE *f) { unsigned char data[3]; int rc; rc = fread(data, 1, 3, f); if (rc != 3) return 0; if (is_mp3_header(data)) return 1; if (fseek(f, -128, SEEK_END) == -1) return 0; rc = fread(data, 1, 3, f); if (rc != 3) return 0; return strncmp((const char *)data, "TAG", 3) == 0; } static int is_mp3_filename(const char *filename) { const char *ext = strrchr(filename, '.'); if (ext == NULL) return 0; return strcasecmp(ext + 1, "mp3") == 0; } static int process_file(const char *filename) { const char *ext = NULL; struct tagger *tagger; struct tagger_ops *ops = NULL; int change; FILE *f; change = !list_empty(&comment_head) || !list_empty(&delete_head) || flag_uniq || flag_clear || flag_pattern; f = fopen(filename, "r"); if (f == NULL) { fprintf(stderr, "%s: error opening file `%s': %s\n", program_name, filename, strerror(errno)); return 1; } else { #if defined(CONFIG_VORBIS) OggVorbis_File vf; if (ov_test(f, &vf, NULL, 0) == 0) { ops = &vorbis_tagger_ops; ext = "ogg"; } else #endif { rewind(f); if (is_mp3(f) || is_mp3_filename(filename)) { ops = &mp3_tagger_ops; ext = "mp3"; /* save file if -id3-write-charset option given and the * charset is not same as the charset used when tags are read */ if (flag_id3_wcs && strcmp(id3_read_charset, id3_write_charset)) change = 1; } } fclose(f); } if (ops == NULL) { fprintf(stderr, "%s: unknown file type: %s\n", program_name, filename); return 1; } if (ops->open(&tagger, filename, change)) return -1; if (change) { LIST_HEAD(state_comment_head); struct list_head *item; if (!flag_clear) { ops->get_comments(tagger, &state_comment_head); comments_remove(&state_comment_head, &delete_head); } if (flag_pattern) { LIST_HEAD(head); parse_filename(&head, filename, flag_pattern); if (flag_conv_usc) underscores_to_spaces(&head); if (flag_capitalize) capitalize(&head); item = head.next; while (item != &head) { struct list_head *next = item->next; list_add_tail(item, &state_comment_head); item = next; } } list_for_each(item, &comment_head) { struct comment *comment, *new; comment = list_entry(item, struct comment, node); new = xnew(struct comment , 1); new->key = xstrdup(comment->key); new->utf8_val = xstrdup(comment->utf8_val); new->val = xstrdup(comment->val); list_add_tail(&new->node, &state_comment_head); /* printf("adding comment %s=%s\n", new->key, new->val); */ } if (flag_uniq) comments_uniq(&state_comment_head); ops->set_comments(tagger, &state_comment_head); /* save */ if (flag_pretend) { LIST_HEAD(head); printf("saving (pretending) %s\n", filename); ops->get_comments(tagger, &head); comments_print(&head); comments_free(&head); printf("saved (not really)\n"); } else { if (ops->save(tagger)) { return 1; } } if (flag_rename) { char *new_filename; replace_invalid_chars(&state_comment_head); if (flag_annoying) replace_annoying_chars(&state_comment_head); if (flag_lowercase) lowercase(&state_comment_head); new_filename = make_filename(&state_comment_head, flag_rename, ext); if (rename_file_or_pretend(filename, new_filename)) { return 1; } free(new_filename); } comments_free(&state_comment_head); } else if (flag_rename) { LIST_HEAD(state_comment_head); char *new_filename; ops->get_comments(tagger, &state_comment_head); replace_invalid_chars(&state_comment_head); if (flag_annoying) replace_annoying_chars(&state_comment_head); if (flag_lowercase) lowercase(&state_comment_head); new_filename = make_filename(&state_comment_head, flag_rename, ext); /* move filename to new_filename */ if (rename_file_or_pretend(filename, new_filename)) { return 1; } free(new_filename); comments_free(&state_comment_head); } else { LIST_HEAD(state_comment_head); ops->get_comments(tagger, &state_comment_head); printf("\n%s:\n", filename); comments_print(&state_comment_head); comments_free(&state_comment_head); } ops->close(tagger); return 0; } static void list_id3_genres(void) { int i; for (i = 0; i < NR_GENRES; i++) printf("%3d: %s\n", i, genres[i]); } static const char *help = "Usage: %s OPTION... FILE...\n" " or: %s FILE...\n" "Change or display MP3 and Ogg/Vorbis meta data tags.\n" "\n" " -artist TEXT name of the artist\n" " -album TEXT name of the album\n" " -disc TEXT disc number\n" " -title TEXT track title\n" " -track TEXT track number\n" " -genre TEXT music genre\n" " -date TEXT release date\n" " -tag TAG=TEXT set any tag\n" " -uniq remove duplicate comments (usefull for vorbis only)\n" " ogg files can have multiple tags with same name\n" " -clear remove all tags\n" " -delete TAG delete tag\n" "\n" " -pattern FORMAT parse tag information from filename\n" " see -usage for more information\n" " -capitalize capitalize words taken from filename\n" " -underscores convert underscores taken from filename to spaces\n" "\n" " -rename FORMAT rename file creating any missing path components.\n" " see -usage for more information\n" " -annoying replace annoying chars from filename\n" " works with -rename only\n" " -lowercase convert filename to lowercase\n" " works with -rename only\n" "\n" " -pretend display changes instead of modifying/renaming files\n" "\n" " -id3-read-charset CS use this charset when reading ID3 (.mp3) tags\n" " -id3-write-charset CS use this charset when writing ID3 (.mp3) tags \n" " If write charset is given and is not same as\n" " the charset used when tags were read then file\n" " will be saved unless -pretend flag is used.\n" " -id3-genres display all available ID3 (for .mp3 files) genres and exit\n" " for .ogg files any genre is valid\n" "\n" " -help display this help and exit\n" " -usage display usage examples and exit. PLEASE READ!\n" " -version output version information and exit\n" "\n" "Options can be shortened if they are unambiguous, e.g. -ta = -tag.\n" "Please execute `%s -usage' and read all output. You have been warned.\n" "Always use -pretend flag unless you really understand what you are doing.\n" "\n" "Report bugs to .\n"; static const char *usage = "Format of FORMAT argument (-rename and -pattern)\n" " %%a artist\n" " %%l album\n" " %%t title\n" " %%n track number\n" " %%g genre\n" " %%d release date (must be year for mp3 files)\n" " %%e filename extension (ogg or mp3)\n" " %%* matches anything (use only in -pattern format string)\n" " %%%% literal '%%'\n" "\n" " When renaming files the FORMAT can contain slashes (e.g. '%%a/%%l/%%t.ogg')\n" " but when taking tag information from filename (-pattern) the FORMAT string\n" " must NOT contain slashes -- pattern is matched against file part of the\n" " filename (path removed).\n" "\n" "Examples:\n" " 1. Show tags:\n" " tagger foo.ogg\n" "\n" " 2. Pretend to delete tags named 'comment' (case insensitive) and then pretend\n" " to rename file. Filename will be lowercased (-l) and characters that\n" " would need to be escaped would be replaced with '_' or '-'. Note the '.%%e'\n" " at end of the filename pattern. This makes it possible to rename mp3 and\n" " ogg files with single command.\n" "\n" " tagger -pr -de comment -r '%%a/%%l/%%n-%%t.%%e' -an -l *.mp3 *.ogg\n" "\n" " 3. Assuming that you have files '01-some_title.ogg' and '02-other_title.ogg'\n" " and you run this command:\n" "\n" " tagger -uniq -pa '%%n-%%t.ogg' -ca -und -date 2004 *.ogg\n" "\n" " Now the first file will have these new tags:\n" " TRACKNUMBER=1, TITLE='Some Title', DATE=2004\n" "\n" " and the second file will have these new tags:\n" " TRACKNUMBER=2, TITLE='Other Title', DATE=2004\n" "\n" " All but the last comment with duplicate name will be deleted (-uniq flag).\n" "\n" " Notes: Delete and clear are always the first actions performed. That means\n" " you can clear all tags and then add tags to files with one command.\n" "\n" " Rename is always the last action performed so you can modify tags and\n" " then use this new tag information to rename files in just one command.\n" "\n" " Tag names are case insensitive and are converted to uppercase before\n" " saving. Any tags not containing an '=' are automatically removed and\n" " a warning is displayed.\n" "\n" " For MP3 files only ID3v1.1 is supported. This means you can only\n" " have 30 characters in ARTIST, ALBUM and TITLE tags, 4 digit YEAR,\n" " 28 characters in COMMENT (who cares), one of the predefined genres and\n" " no other tags.\n" "\n" " Default charset for ID3 tags is %s.\n"; enum { OPT_ARTIST, OPT_ALBUM, OPT_DISC, OPT_TITLE, OPT_TRACK, OPT_GENRE, OPT_DATE, OPT_TAG, OPT_UNIQ, OPT_CLEAR, OPT_DELETE, OPT_PATTERN, OPT_CAPITALIZE, OPT_UNDERSCORES, OPT_RENAME, OPT_ANNOYING, OPT_LOWERCASE, OPT_PRETEND, OPT_ID3RCS, OPT_ID3WCS, OPT_ID3GENRES, OPT_HELP, OPT_USAGE, OPT_VERSION, NUM_OPTIONS }; static struct option options[NUM_OPTIONS + 1] = { { "artist", 1 }, { "album", 1 }, { "disc", 1 }, { "title", 1 }, { "track", 1 }, { "genre", 1 }, { "date", 1 }, { "tag", 1 }, { "uniq", 0 }, { "clear", 0 }, { "delete", 1 }, { "pattern", 1 }, { "capitalize", 0 }, { "underscores", 0 }, { "rename", 1 }, { "annoying", 0 }, { "lowercase", 0 }, { "pretend", 0 }, { "id3-read-charset", 1 }, { "id3-write-charset", 1 }, { "id3-genres", 0 }, { "help", 0 }, { "usage", 0 }, { "version", 0 }, { NULL, 0 } }; static int option_handler(int opt, const char *arg) { switch (opt) { case OPT_ARTIST: comment_add(&comment_head, "ARTIST", arg); break; case OPT_ALBUM: comment_add(&comment_head, "ALBUM", arg); break; case OPT_DISC: comment_add(&comment_head, "DISCNUMBER", arg); break; case OPT_TITLE: comment_add(&comment_head, "TITLE", arg); break; case OPT_TRACK: comment_add(&comment_head, "TRACKNUMBER", arg); break; case OPT_GENRE: comment_add(&comment_head, "GENRE", arg); break; case OPT_DATE: comment_add(&comment_head, "DATE", arg); break; case OPT_TAG: { char *key, *val; if (line_to_key_value(arg, &key, &val)) return -1; comment_add(&comment_head, key, val); free(key); free(val); } break; case OPT_UNIQ: flag_uniq = 1; break; case OPT_CLEAR: flag_clear = 1; break; case OPT_DELETE: { struct comment *comment; comment = (struct comment *)xmalloc(sizeof(struct comment)); comment->key = xstrdup(arg); comment->utf8_val = NULL; comment->val = NULL; list_add_tail(&comment->node, &delete_head); } break; case OPT_PATTERN: validate_parse_pattern(arg); flag_pattern = arg; break; case OPT_CAPITALIZE: flag_capitalize = 1; break; case OPT_UNDERSCORES: flag_conv_usc = 1; break; case OPT_RENAME: validate_rename_pattern(arg); flag_rename = arg; break; case OPT_ANNOYING: flag_annoying = 1; break; case OPT_LOWERCASE: flag_lowercase = 1; break; case OPT_PRETEND: flag_pretend = 1; break; case OPT_ID3RCS: id3_read_charset = arg; break; case OPT_ID3WCS: id3_write_charset = arg; flag_id3_wcs = 1; break; case OPT_ID3GENRES: list_id3_genres(); exit(0); break; case OPT_HELP: printf(help, program_name, program_name, program_name); exit(0); break; case OPT_USAGE: printf(usage, default_id3_charset); exit(0); break; case OPT_VERSION: printf("tagger " VERSION "\n"); exit(0); break; } return 0; } int main(int argc, char *argv[]) { char **args = argv + 1; program_name = argv[0]; setlocale(LC_CTYPE, ""); charset = nl_langinfo(CODESET); id3_read_charset = default_id3_charset; id3_write_charset = default_id3_charset; options_parse(&args, options, option_handler, NULL); if (*args == NULL) { fprintf(stderr, "%s: missing file argument\n" "Try `%s -help' for more information.\n", program_name, program_name); return 1; } while (*args) { int rc; rc = process_file(*args++); if (rc) return rc; } comments_free(&comment_head); comments_free(&delete_head); return 0; }