/* fixmp3s.cpp Utility to fix my broken MP3 files. Works for me, YMMV. Cobbled together to fix my MP3 collection after iTunes 8.2 decided that 5000+ of my MP3 files no longer had images associated with them, and in some cases no longer had album/artist/song information. As a bonus, this program finally allows my Nokia phone to display album art. (Nokia software is truly awful, I regret getting my N85.) I am not responsible for this program corrupting your MP3 files - make sure you KEEP BACKUPS OF YOUR MP3 FILES BEFORE USING THIS PROGRAM! Uses id3lib from http://id3lib.sourceforge.net. Unfortunately id3lib appears to be orphaned and has a few bugs. Apply the following patch (fixing Unicode output) to io_helpers.cpp before doing the "configure/sudo make install" incantation. http://sourceforge.net/tracker/index.php?func=detail&aid=1016290&group_id=979&atid=300979 I built this on my Mac via c++ fixmp3s.c++ -O -Wall -lid3 -lz -o ./fixmp3s Use 'fixmp3s --help' to see help. Note that fixmp3s will *NOT* write any changes back to disk unless the "--update" flag is passed. Example usage: find . \( -name \*.mp3 -or -name \*.MP3 \) -type f -print0 | xargs -0 fixmp3s --cover=1 --update ... will search under the current directory for *.mp3 or *.MP3 files and run fixmp3s on them. fixmp3s will by default fix the album art MIME type and convert ID3V1 to ID3V2 if required. The "--cover=1" option sets the FRONT_COVER to the first embedded image (in case there are multiple embedded images in an MP3 file.) If you don't specify "--update", changes made by fixmp3s will be lost (ie, you can see what changes it would have made.) This should work on most Unices and possibly cygwin. Version History ============================================================ 1.1 20090623 turly Update usage. 1.0 20090621 turly Initial release. ============================================================ Suggestions or bug reports? Let me know: turly@finderpop.com */ #include #include #define FIXMP3S_VERSION "1.1" static const int endian_int = 1; #define BIGENDIAN_P() ((*(char*)&endian_int) == 0) /* Old-style ID3 records live in a 128-byte block at EOF. */ typedef struct { unsigned char id3 [3]; // "TAG" char song_name [30]; char artist_name [30]; char album_name [30]; char album_year [4]; char note [30]; unsigned char style; } old_id3_tag_rec_t; // Used for debuggery. static const char *_current_filename = NULL; // Ick. id3lib may already provide this, but I can't find it :-) #define NUMERIC_CASE(X) case X : return #X + 6 const char *FieldIdToString (ID3_FieldID fid) { switch (fid) { NUMERIC_CASE (ID3FN_NOFIELD); NUMERIC_CASE (ID3FN_TEXTENC); NUMERIC_CASE (ID3FN_TEXT); NUMERIC_CASE (ID3FN_URL); NUMERIC_CASE (ID3FN_DATA); NUMERIC_CASE (ID3FN_DESCRIPTION); NUMERIC_CASE (ID3FN_OWNER); NUMERIC_CASE (ID3FN_EMAIL); NUMERIC_CASE (ID3FN_RATING); NUMERIC_CASE (ID3FN_FILENAME); NUMERIC_CASE (ID3FN_LANGUAGE); NUMERIC_CASE (ID3FN_PICTURETYPE); NUMERIC_CASE (ID3FN_IMAGEFORMAT); NUMERIC_CASE (ID3FN_MIMETYPE); default: return ""; } } static void dump_current_filename_once (void) { if (_current_filename != NULL) { printf ("%s\n", _current_filename); _current_filename = NULL; // don't dump the filename again } } static void check_text_frame (ID3_Tag &tg, const char *what, ID3_FrameID frameID, const char *text) { ID3_Frame *frm = tg.Find (frameID); if (frm == NULL) // not there! { dump_current_filename_once (); printf (" Does not contain %s ID3v2 tag", what); if (text [0]) { frm = new ID3_Frame (frameID); if (frm != NULL) { frm->GetField (ID3FN_TEXT)->Set(text); tg.AttachFrame (frm); printf (" - added '%s'", text); } } printf ("\n"); } } int Usage (int exitcode) { printf ("Usage: fixmp3s OPTIONS FILENAME [FILENAME...]\n"); printf ("Options include:\n"); printf (" --help this help\n"); printf (" --dump textually dump the ID3 tags found in each file specified\n"); printf (" --verbose print progress\n"); printf (" --[no]v1tov2 [don't] convert ID3V1 tags to V2 if no V2 tags present\n"); printf (" --[no]mime [don't] fixup album art MIME type\n"); printf (" --[no]cover [don't] set album art type to 3 (FRONT COVER)\n"); printf (" --[no]cover=N [don't] set Nth embedded image as FRONT_COVER\n"); printf (" --[no]update [don't] write back changes\n"); printf (" --warn-no-art scan file for album art and warn if none present\n"); printf (" NOTE: no fixes will be applied until you specify --update\n"); printf ("\n"); printf (" turly@finderpop.com cobbled together this program to fix his MP3\n"); printf (" collection after iTunes 8.2 decided that 5000+ of his MP3 files no\n"); printf (" longer had images associated with them, and in some cases no longer\n"); printf (" had album/artist/song information. As a bonus, this program finally\n"); printf (" allowed his Nokia phone to display album art.\n"); printf (" Fixes applied :\n"); printf (" --v1tov2 if V1 tags present but no V2 tags, create V2 using V1 tags\n"); printf (" --mime Fix album art MIME type string to 'image/jpeg' or 'image/png'\n"); printf (" --cover Set album art picture type to FRONT_COVER\n"); printf (" (NOTE: MP3 files with multiple embedded images will not have\n"); printf (" picture type fixed unless you specify --cover=N)\n"); printf ("\n"); printf (" NOTE: The author is not responsible for possibly corrupting MP3 files.\n"); printf (" Always ensure you have backups in case things go wrong!\n"); printf ("\nfixmp3s version " FIXMP3S_VERSION " built on " __DATE__ " " __TIME__ "\n"); printf ("Uses a patched version of id3lib from http://id3lib.sourceforge.net\n"); exit (exitcode); } int main (int argc, const char *argv []) { old_id3_tag_rec_t rec; int ix; char old_artist_name [32 + 1]; char old_album_name [32 + 1]; char old_song_name [32 + 1]; bool dump_p = false; bool verbose_p = false; bool write_p = false; bool fix_p = true; bool v1tov2_p = true; bool fix_mime_p = true; bool fix_cover_p = true; bool warn_no_art_p = false; int cover_image_num = 0; unsigned cover_val = ID3PT_COVERFRONT; for (ix = 1; ix < argc; ++ix) { const char *arg = argv [ix]; if (arg [0] == '-') // option... { // Use getopts ffs! if (strcmp (arg, "--dump") == 0 || strcmp (arg, "--nodump") == 0) { dump_p = (arg [2] == 'd'); fix_p = ! dump_p; } else if (strcmp (arg, "-v") == 0 || strcmp (arg, "--verbose") == 0) verbose_p = true; else if (strcmp (arg, "--warn-no-art") == 0 || strcmp (arg, "--no-warn-no-art") == 0) warn_no_art_p = (arg [2] == 'w'); else if (strcmp (arg, "--quiet") == 0) verbose_p = dump_p = false; else if (strcmp (arg, "--update") == 0 || strcmp (arg, "--noupdate") == 0) write_p = (arg [2] == 'u'); else if (strcmp (arg, "--mime") == 0 || strcmp (arg, "--nomime") == 0) fix_mime_p = (arg [2] == 'm'); else if (strncmp (arg, "--cover=", 8) == 0 || strncmp (arg, "--nocover=", 10) == 0) { fix_cover_p = true; cover_val = (arg [2] == 'c') ? ID3PT_COVERFRONT : 0; cover_image_num = atoi (arg + ((arg [7] == '=') ? 8 : 10)); } else if (strcmp (arg, "--cover") == 0 || strcmp (arg, "--nocover") == 0) fix_cover_p = (arg [2] == 'c'); else if (strcmp (arg, "--v1tov2") == 0 || strcmp (arg, "--nov1tov2") == 0) v1tov2_p = (arg [2] == 'v'); else if (strcmp (arg, "--help") == 0) Usage (0); else { printf ("fixmp3s: Unrecognised arg '%s'\n", arg); Usage (-1); } } else // file { ID3_Tag tg; memset (&rec, 0, sizeof (rec)); memset (old_artist_name, 0, sizeof (old_artist_name)); memset (old_album_name, 0, sizeof (old_album_name)); memset (old_song_name, 0, sizeof (old_song_name)); _current_filename = NULL; FILE *fp = fopen (arg, "rb"); if (fp != NULL) { fseek (fp, -128, SEEK_END); // to EOF - 128 int nread = fread (&rec, sizeof (rec), 1, fp); fclose (fp); if (1 == nread) { if (rec.id3 [0] == 'T' && rec.id3 [1] == 'A' && rec.id3 [2] == 'G') { strncpy (old_artist_name, rec.artist_name, sizeof (rec.artist_name)); strncpy (old_song_name, rec.song_name, sizeof (rec.song_name)); strncpy (old_album_name, rec.album_name, sizeof (rec.album_name)); } } } else { fprintf (stderr, "# Couldn't open '%s'\n", arg); return -1; } try { tg.Link (arg, ID3TT_ID3V2); _current_filename = arg; if (verbose_p || dump_p) dump_current_filename_once (); if (fix_p) { if (v1tov2_p) { // If we only have ancient tags, convert them to ID3v2 tags check_text_frame (tg, "album", ID3FID_ALBUM, old_album_name); check_text_frame (tg, "artist", ID3FID_LEADARTIST, old_artist_name); check_text_frame (tg, "song", ID3FID_TITLE, old_song_name); } ID3_Frame *picframe, *firstpic = NULL; unsigned thispicnum = 1, npics = 0; // What a weird API. ID3_Frame *pic1 = tg.Find (ID3FID_PICTURE); if (pic1 != NULL) { do { ++npics; } while (tg.Find (ID3FID_PICTURE) != pic1); if (npics > 1) // wrap back around to first { int countdown = npics - 1; while (countdown--) tg.Find (ID3FID_PICTURE); } } // If we're just looking for art-free mp3 files, check // and do no further processing on this file. if (warn_no_art_p) { if (pic1 == NULL) { dump_current_filename_once (); printf (" DOES NOT CONTAIN ANY ALBUM ART\n"); } continue; // don't process this file any further } while ((picframe = tg.Find (ID3FID_PICTURE)) != firstpic) { if (firstpic == NULL) firstpic = picframe; ID3_Field *mime, *pictype; //printf ("Pic %u: size %lu\n", thispicnum, picframe->Size ()); // Check that the MIME type is correct, also the picture type. if (fix_mime_p && (mime = picframe->GetField (ID3FN_MIMETYPE)) != NULL) { const char *mt = mime->GetRawText (); if (mt [0] == 0) // Bugger, need to figure out the image format { const char *image_type_str = NULL; ID3_Field *picfield = picframe->GetField (ID3FN_DATA); if (picfield != NULL) { const unsigned char *picdata = picfield->GetRawBinary (); // JPEG header bytes if (picdata [0] == 0xFF && picdata [1] == 0xD8 && picdata [2] == 0xFF) // && picdata [3] == 0xE0) { image_type_str = "image/jpeg"; } else if (picdata [0] == 0x89 && picdata [1] == 'P' && picdata [2] == 'N' && picdata [3] == 'G') { image_type_str = "image/png"; } } if (image_type_str != NULL) { mime->Set (image_type_str); dump_current_filename_once (); printf (" Set APIC image MIME type to %s\n", image_type_str); } } else if (verbose_p) printf (" (Embedded image already has MIME type '%s')\n", mime->GetRawText ()); } if (fix_cover_p && (pictype = picframe->GetField (ID3FN_PICTURETYPE)) != NULL) { if (cover_image_num > 0) // setting individual image to COVER_VAL { // If this is the image we're interested in, set the image type. // If this is NOT the image we're interested in // If we're setting a new FRONT_COVER and this image is already a // front cover, then set this image to {unassigned}. if (thispicnum == (unsigned) cover_image_num) { if (pictype->Get () != cover_val) { dump_current_filename_once (); printf (" Setting image #%u of %u (size %lu) to have cover type %u\n", thispicnum, npics, picframe->Size (), cover_val); pictype->Set (cover_val); } } else if (cover_val == ID3PT_COVERFRONT && pictype->Get () == ID3PT_COVERFRONT) { dump_current_filename_once (); printf (" Image #%u (size %lu) was COVERFRONT, now unset (0)\n", thispicnum, picframe->Size ()); pictype->Set ((uint32) 0); } } else if (pictype->Get () == 0) { dump_current_filename_once (); if (npics == 1) { printf (" Setting APIC image type to COVER_FRONT\n"); pictype->Set (ID3PT_COVERFRONT); } else printf (" Not setting APIC image type to COVER_FRONT (image %u of %u)\n", thispicnum, npics); } else if (verbose_p) printf (" (Embedded image of type '%d')\n", pictype->Get ()); } ++thispicnum; } // have PIC frame } // fix_p if (dump_p) { ID3_Tag::Iterator *frame_iter = tg.CreateIterator (); ID3_Frame *frame = NULL; while (NULL != (frame = frame_iter->GetNext ())) { const char *desc = frame->GetDescription (); ID3_TextEnc enc = ID3TE_NONE; printf (" %s %s\n", frame->GetTextID (), (desc) ? desc : ""); ID3_Frame::Iterator *field_iter = frame->CreateIterator (); ID3_Field *field = NULL; while (NULL != (field = field_iter->GetNext ())) { printf (" ID: %2u %s TYPE: %2u Value: ", field->GetID (), FieldIdToString (field->GetID ()), field->GetType ()); if (field->GetID () == ID3FN_TEXTENC) enc = (ID3_TextEnc) field->Get (); switch (field->GetType ()) { case ID3FTY_INTEGER: printf ("%u", field->Get ()); break; case ID3FTY_BINARY: unsigned nbytes = field->Size (); const uchar *p = field->GetRawBinary (); printf ("{ %u bytes: ", nbytes); for (unsigned bx = 0; bx < ((nbytes > 4) ? 4 : nbytes); ++bx) printf ("%02x ", p [bx]); if (nbytes > 4) printf ("..."); printf ("}"); break; case ID3FTY_TEXTSTRING: if (ID3TE_IS_SINGLE_BYTE_ENC (enc)) { char buf [1024]; field->Get (buf, sizeof (buf)); printf ("'%s'", buf); } #if 1 // I know nothing about Unicode -- and it shows :-) else { unicode_t wbuf [1024]; int nchars; nchars = field->Get (wbuf, 1024); putchar ('\''); for (int bx = 0; bx < nchars; ++bx) { unicode_t ch = wbuf [bx]; unicode_t swappedch = ch; if ((enc == ID3TE_UTF16BE) != BIGENDIAN_P ()) swappedch = ((ch >> 8) & 0xFF) | ((ch & 0xFF) << 8); if (swappedch < 128) putchar (swappedch); else printf ("\\%02x%02x", (ch >> 8) & 0xFF, ch & 0xFF); } putchar ('\''); } #endif break; default: break; } printf ("\n"); } delete field_iter; } delete frame_iter; } } catch (...) { fprintf (stderr, "\nfixmp3s: Exception caught, aborting\n"); return -1; } if (write_p) tg.Update (); } } return 0; }