/* FixID3Tag.c

   Objective: Remove the PCST, TGID, WFED and TDES tags that Apple iTunes
   puts in mp3s taken from podcasts. TGID and WFED and valid and legal,
   but they contain [unnecessary] information about the podcast and where
   the mp3 was obtained from. PCST is entirely new and causes iTunes to
   enable 'bookmarking' in the mp3 file.

   -- ID3v2.3 Header ------------------------
   |                                        |
   |  File Identifier  "ID3"                |
   |  ID3v2 Version    03 00                |
   |  Flags            xx                   |
   |  ID3v2 Size       xx xx xx xx          |
   ------------------------------------------

   -- ID3v2.3 Frame Overview ----------------
   |                                        |
   |  Frame ID  $xx xx xx xx (four bytes)   |
   |  Size      $xx xx xx xx                |
   |  Flags     $xx xx                      |
   ------------------------------------------

   Source: http://www.id3.org/
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ID3_HEADER_CONSTANT     3 // Skip the File Identifier
#define ID3_HEADER_SIZE         4
#define ID3_HEADER_VERSION      2
#define ID3_HEADER_FLAGS        1
#define ID3_HEADER_TOTAL        10

#define ID3_2_2_FRAME_ID        3
#define ID3_2_2_FRAME_SIZE      3
#define ID3_2_2_FRAME_FLAGS     0
#define ID3_2_2_FRAME_TOTAL     6

#define ID3_2_3_FRAME_ID        4
#define ID3_2_3_FRAME_SIZE      4
#define ID3_2_3_FRAME_FLAGS     2
#define ID3_2_3_FRAME_TOTAL     10

#define ID3_2_4_FRAME_ID        4
#define ID3_2_4_FRAME_SIZE      4
#define ID3_2_4_FRAME_FLAGS     2
#define ID3_2_4_FRAME_TOTAL     10

void stripFromVersion2_2(FILE *in, long header_size);
void stripFromVersion2_3(FILE *in, long header_size);
void stripFromVersion2_4(FILE *in, long header_size);

int main(int argc, char *argv[])
{
    FILE *input                                          = NULL;
    unsigned char id3_header_size[ID3_HEADER_SIZE]       = {0x00, 0x00, 0x00, 0x00};
    unsigned char id3_header_version[ID3_HEADER_VERSION] = {0x00, 0x00};
    int id3_version                                      = 0;
    int i;

    /* Make sure we're called with the proper arguments */
    if (argc < 2)
    {
        printf("Usage: %s <mp3-file(s)>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    for (i = 1; i < argc; i++)
    {
        long id3_header_size_long = 0;

        /* Open the file for read/write access. Set cursor to beginning of file */
        input = fopen(argv[i], "r+");
        if (input == NULL)
        {
            printf("Unable to open file for read/write access.\n");
            exit(EXIT_FAILURE);
        }

        /* Skip over the predefined ID3 tag part that never changes */
        if (fseek(input, ID3_HEADER_CONSTANT, SEEK_SET))
        {
            printf("Error seeking in file.\n");
            exit(EXIT_FAILURE);
        }

        /* Read in the ID3 Header Version Number */
        if (!fread(id3_header_version, 1, ID3_HEADER_VERSION, input))
        {
            printf("Error reading in Header Version Number.\n");
            exit(EXIT_FAILURE);
        }

        /* Set the version of this ID3 Header.
         *   2 = ID3v2.2
         *   3 = ID3v2.3
         *   4 = ID3v2.4
         */
        id3_version = id3_header_version[0];

        /* Skip over the flags field */
        if (fseek(input, ID3_HEADER_FLAGS, SEEK_CUR))
        {
            printf("Error seeking in file.\n");
            exit(EXIT_FAILURE);
        }

        /* Read in the ID3 Header Size */
        if (!fread(id3_header_size, 1, ID3_HEADER_SIZE, input))
        {
            printf("Error reading ID3 header size.\n");
            exit(EXIT_FAILURE);
        }

        id3_header_size_long += id3_header_size[0] << 21;
        id3_header_size_long += id3_header_size[1] << 14;
        id3_header_size_long += id3_header_size[2] << 7;
        id3_header_size_long += id3_header_size[3];

        printf("%s:\n-----------------\n", argv[i]);

        if (id3_version == 2)
            stripFromVersion2_2(input, id3_header_size_long);
        else if (id3_version == 3)
            stripFromVersion2_3(input, id3_header_size_long);
		else if (id3_version == 4)
			stripFromVersion2_4(input, id3_header_size_long);
        else
            printf("The ID3 header version number %d is not currently supported.\n", id3_version);

        printf("-----------------\n");

        fclose(input);
    }

    return 0;
}

void stripFromVersion2_2(FILE *in, long header_size)
{
    long frame_size_long                    = 0;
    int total_removed                       = 0;

    long j                                  = 0;
    int found_frame                         = 0;

    unsigned char *header_data              = NULL;
    
    /* Read in the full header to memory */
    header_data = malloc(header_size);
    if (!fread(header_data, 1, header_size, in))
    {
        printf("Error reading ID3 frame.\n");
        exit(EXIT_FAILURE);
    }

    /* Read in the ID3 tag header frames */
    while ( j < header_size - total_removed )
    {
        /* Calculate the frame size */
        frame_size_long = header_data[j + 3] << 8;
        frame_size_long = (frame_size_long | header_data[j + 4]) << 8;
        frame_size_long |= header_data[j + 5];
            
        /* If the frame header is PCS, TID, or WFD ... */
        if ((strncmp((char *)header_data + j, "PCS", ID3_2_2_FRAME_ID) == 0) ||
            (strncmp((char *)header_data + j, "TID", ID3_2_2_FRAME_ID) == 0) ||
            (strncmp((char *)header_data + j, "WFD", ID3_2_2_FRAME_ID) == 0))
        {
            printf("Removed:\n");
            printf(" frame_size = %ld\n", frame_size_long);
            printf(" id3_frame = %c%c%c\n", header_data[j], header_data[j + 1], header_data[j + 2]);

            /* Shift all the tags after this tag up */
            memcpy(header_data + j, 
                   header_data + j + frame_size_long + ID3_2_2_FRAME_TOTAL, 
                   header_size - (j + frame_size_long + ID3_2_2_FRAME_TOTAL));

            /* Calculate how many bytes we removed */
            total_removed += frame_size_long + ID3_2_2_FRAME_TOTAL;

            /* Indicate that we found a 'bad' frame */
            found_frame++;
        }
        else
        {
            j += frame_size_long + ID3_2_2_FRAME_TOTAL;
        }
    }

    /* Since we shifted all of the other tags up, we want to zero out the bytes where they were before */
    memset(header_data + (header_size - total_removed), 0, total_removed);

    /* Seek to the beginning of the file and jump over the constant ID3 header portion */
    fseek(in, ID3_HEADER_TOTAL, 0);

    /* Copy the new ID3 header overtop of the existing one in the file */
    fwrite(header_data, header_size, 1, in);

    if (found_frame)
        printf("\nSuccessful.\n");
    else
        printf("This file does not appear to have any podcast information stored in it.\n");
}

void stripFromVersion2_3(FILE *in, long header_size)
{
    long frame_size_long            = 0;
    int total_removed               = 0;

    long j                          = 0;
    int found_frame                 = 0;

    unsigned char *header_data      = NULL;

    /* Read in the full header to memory */
    header_data = malloc(header_size);
    if (!fread(header_data, 1, header_size, in))
    {
        printf("Error reading ID3 frame.\n");
        exit(EXIT_FAILURE);
    }

    while ( j < header_size - total_removed )
    {
        /* Calculate the frame size */
        frame_size_long = header_data[j + 4] << 8;
        frame_size_long = (frame_size_long | header_data[j + 5]) << 8;
        frame_size_long = (frame_size_long | header_data[j + 6]) << 8;
        frame_size_long |= header_data[j + 7];

        /* If the frame header is PCST, TGID, WFED, TDES, or TCON ... */
        if ((strncmp((char *)header_data + j, "PCST", ID3_2_3_FRAME_ID) == 0) ||
            (strncmp((char *)header_data + j, "TGID", ID3_2_3_FRAME_ID) == 0) ||
            (strncmp((char *)header_data + j, "WFED", ID3_2_3_FRAME_ID) == 0) ||
			(strncmp((char *)header_data + j, "TDES", ID3_2_3_FRAME_ID) == 0) ||
			(strncmp((char *)header_data + j, "TCON", ID3_2_3_FRAME_ID) == 0))
        {
            printf("Removed:\n");
            printf(" frame_size = %ld\n", frame_size_long);
            printf(" id3_frame = %c%c%c%c\n", header_data[j], header_data[j + 1], header_data[j + 2], header_data[j + 3]);

            /* Shift all the tags after this tag up */
            memcpy(header_data + j, 
                   header_data + j + frame_size_long + ID3_2_3_FRAME_TOTAL, 
                   header_size - (j + frame_size_long + ID3_2_3_FRAME_TOTAL));

            /* Calculate how many bytes we removed */
            total_removed += frame_size_long + ID3_2_3_FRAME_TOTAL;

            /* Indicate that we found a 'bad' frame */
            found_frame++;
        }
        else
        {
            j += frame_size_long + ID3_2_3_FRAME_TOTAL;
        }
    }

    /* Since we shifted all of the other tags up, we want to zero out the bytes where they were before */
    memset(header_data + (header_size - total_removed), 0, total_removed);

    /* Seek to the beginning of the file and jump over the constant ID3 header portion */
    fseek(in, ID3_HEADER_TOTAL, 0);

    /* Copy the new ID3 header overtop of the existing one in the file */
    fwrite(header_data, header_size, 1, in);

    if (found_frame)
        printf("\nSuccessful.\n");
    else
        printf("This file does not appear to have any podcast information stored in it.\n");
}

void stripFromVersion2_4(FILE *in, long header_size)
{
    long frame_size_long            = 0;
    int total_removed               = 0;

    long j                          = 0;
    int found_frame                 = 0;

    unsigned char *header_data      = NULL;

    /* Read in the full header to memory */
    header_data = malloc(header_size);
    if (!fread(header_data, 1, header_size, in))
    {
        printf("Error reading ID3 frame.\n");
        exit(EXIT_FAILURE);
    }

    while ( j < header_size - total_removed )
    {
        /* Calculate the frame size */
        frame_size_long = header_data[j + 4] << 7;
        frame_size_long = (frame_size_long | header_data[j + 5]) << 7;
        frame_size_long = (frame_size_long | header_data[j + 6]) << 7;
        frame_size_long |= header_data[j + 7];

        /* If the frame header is PCST, TGID, WFED, TDES, or TCON ... */
        if ((strncmp((char *)header_data + j, "PCST", ID3_2_4_FRAME_ID) == 0) ||
            (strncmp((char *)header_data + j, "TGID", ID3_2_4_FRAME_ID) == 0) ||
            (strncmp((char *)header_data + j, "WFED", ID3_2_4_FRAME_ID) == 0) ||
			(strncmp((char *)header_data + j, "TDES", ID3_2_4_FRAME_ID) == 0) ||
			(strncmp((char *)header_data + j, "TCON", ID3_2_4_FRAME_ID) == 0))
        {
            printf("Removed:\n");
            printf(" frame_size = %ld\n", frame_size_long);
            printf(" id3_frame = %c%c%c%c\n", header_data[j], header_data[j + 1], header_data[j + 2], header_data[j + 3]);

            /* Shift all the tags after this tag up */
            memcpy(header_data + j, 
                   header_data + j + frame_size_long + ID3_2_4_FRAME_TOTAL, 
                   header_size - (j + frame_size_long + ID3_2_4_FRAME_TOTAL));

            /* Calculate how many bytes we removed */
            total_removed += frame_size_long + ID3_2_4_FRAME_TOTAL;

            /* Indicate that we found a 'bad' frame */
            found_frame++;
        }
        else
        {
            j += frame_size_long + ID3_2_4_FRAME_TOTAL;
        }
    }

    /* Since we shifted all of the other tags up, we want to zero out the bytes where they were before */
    memset(header_data + (header_size - total_removed), 0, total_removed);

    /* Seek to the beginning of the file and jump over the constant ID3 header portion */
    fseek(in, ID3_HEADER_TOTAL, 0);

    /* Copy the new ID3 header overtop of the existing one in the file */
    fwrite(header_data, header_size, 1, in);

    if (found_frame)
        printf("\nSuccessful.\n");
    else
        printf("This file does not appear to have any podcast information stored in it.\n");
}
