package org.cmc.music.myid3.id3v2;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;
import org.cmc.music.common.ID3FrameType;
import org.cmc.music.common.ID3WriteException;
import org.cmc.music.metadata.IMusicMetadata;
import org.cmc.music.metadata.ImageData;
import org.cmc.music.metadata.MusicMetadata;
import org.cmc.music.metadata.MusicMetadataSet;
import org.cmc.music.myid3.MyID3Listener;
import org.cmc.music.util.Debug;
public class MyID3v2Write implements MyID3v2Constants
{
private final int id3v2_version = 3;
private byte[] getHeaderFooter(int body_length, boolean is_footer)
throws ID3WriteException
{
byte result[] = new byte[10];
int index = 0;
if (is_footer)
{
result[index++] = 0x33; // 3
result[index++] = 0x44; // D
result[index++] = 0x49; // I
} else
{
result[index++] = 0x49; // I
result[index++] = 0x44; // D
result[index++] = 0x33; // 3
}
if (id3v2_version == 4)
result[index++] = 0x04; // version
else if (id3v2_version == 3)
result[index++] = 0x03; // version
else
throw new ID3WriteException("id3v2_version: " + id3v2_version);
result[index++] = 0x00;
int flags = 0; // charles
if (id3v2_version == 4)
flags |= HEADER_FLAG_ID3v24_FOOTER_PRESENT;
else if (id3v2_version == 3)
{
} else
throw new ID3WriteException("id3v2_version: " + id3v2_version);
result[index++] = (byte) flags;
writeSynchSafeInt(result, index, body_length);
return result;
}
private final void writeSynchSafeInt(byte bytes[], int start, int value)
throws ID3WriteException
{
bytes[start + 3] = (byte) (value & 0x7f);
value >>= 7;
bytes[start + 2] = (byte) (value & 0x7f);
value >>= 7;
bytes[start + 1] = (byte) (value & 0x7f);
value >>= 7;
bytes[start + 0] = (byte) (value & 0x7f);
value >>= 7;
if (value != 0)
throw new ID3WriteException("Value to large for synch safe int: "
+ value);
}
private ID3v2OutputFrame toFrame(String longFrameID, Number frameOrder,
String value1, String value2) throws UnsupportedEncodingException,
IOException
{
if (longFrameID.startsWith("T"))
{
return toFrameText(longFrameID, frameOrder, value1, value2);
} else if (longFrameID.equals("COMM"))
{
return toFrameCOMM(longFrameID, frameOrder, value1);
} else
{
// TODO: should we throw an exception here?
Debug.debug();
Debug.debug("frame_type.long_id", longFrameID);
Debug.debug("not text");
Debug.dumpStack();
return null;
}
}
private static boolean canEncodeStringInISO(String s)
throws UnsupportedEncodingException
{
byte bytes[] = s.getBytes(CHAR_ENCODING_ISO);
String check1 = new String(bytes, CHAR_ENCODING_ISO);
return check1.equals(s);
}
private static byte[] encodeString(String s, boolean use_iso)
throws UnsupportedEncodingException, IOException
{
if (use_iso)
return s.getBytes(CHAR_ENCODING_ISO);
else
{
byte bytes[] = s.getBytes(CHAR_ENCODING_UTF_16);
// Windows Media Player can't handle UTF-16, big-endian.
// switch to UTF-16, little-endian.
if (((0xff & bytes[0]) == 0xFE) && ((0xff & bytes[1]) == 0xFF))
{
// manually switch UTF 16 byte order
for (int i = 0; i < bytes.length; i += 2)
{
byte temp = bytes[i];
bytes[i] = bytes[i + 1];
bytes[i + 1] = temp;
}
}
return bytes;
}
}
// TODO: convert Object params to String params.
private static ID3v2OutputFrame toFrameText(String longFrameID,
Number frameOrder, String value1, String value2)
throws UnsupportedEncodingException, IOException
{
boolean use_iso = canEncodeStringInISO(value1);
if (value2 != null)
use_iso &= canEncodeStringInISO(value2);
int char_encoding_code = use_iso ? CHAR_ENCODING_CODE_ISO_8859_1
: CHAR_ENCODING_CODE_UTF_16_WITH_BOM;
// : CHAR_ENCODING_CODE_UTF_8;
byte string_1_bytes[] = encodeString(value1, use_iso);
byte string_2_bytes[] = null;
if (value2 != null)
string_2_bytes = encodeString(value2, use_iso);
// Debug.debug("use_iso", use_iso);
// Debug.debug("string_1_bytes", string_1_bytes);
// Debug.debug("s2", s2);
// int frame_length = kFRAME_HEADER_LENGTH + string_bytes.length + 1;
int result_length = string_1_bytes.length + 1;
if (string_2_bytes != null)
result_length += string_2_bytes.length + 1;
byte result[] = new byte[result_length];
int index = 0;
result[index++] = (byte) char_encoding_code;
System.arraycopy(string_1_bytes, 0, result, index,
string_1_bytes.length);
index += string_1_bytes.length;
if (string_2_bytes != null)
{
result[index++] = (byte) char_encoding_code;
System.arraycopy(string_2_bytes, 0, result, index,
string_2_bytes.length);
}
return new ID3v2OutputFrame(longFrameID, frameOrder, result);
}
private static ID3v2OutputFrame toFrameImage(String longFrameID,
Number frameOrder, ImageData imageData)
// byte imageData[], String mimeType, String description,
// int pictureType)
throws UnsupportedEncodingException, IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean use_iso = canEncodeStringInISO(imageData.description);
int char_encoding_code = use_iso ? CHAR_ENCODING_CODE_ISO_8859_1
: CHAR_ENCODING_CODE_UTF_16_WITH_BOM;
baos.write(char_encoding_code);
byte mimeTypeBytes[] = encodeString(imageData.mimeType, true);
// Debug.debug("mimeType", mimeType);
// Debug.debug("mimeTypeBytes", mimeTypeBytes);
baos.write(mimeTypeBytes);
baos.write(0);
baos.write(0xff & imageData.pictureType);
byte descriptionBytes[] = encodeString(imageData.description, use_iso);
baos.write(descriptionBytes);
// Debug.debug("description", description);
// Debug.debug("descriptionBytes", descriptionBytes);
baos.write(0);
baos.write(imageData.imageData);
byte frameBytes[] = baos.toByteArray();
// Debug.debug("frameBytes", frameBytes);
return new ID3v2OutputFrame(longFrameID, frameOrder, frameBytes);
}
private static class FrameFactory implements IFrameFactory
{
public ID3v2OutputFrame createUserTextFrame(String value1, String value2)
throws UnsupportedEncodingException, IOException
{
ID3FrameType frameType = ID3FrameType.USERTEXT;
String longFrameID = frameType.longID;
Number frameOrder = frameType.getFrameOrder();
return toFrameText(longFrameID, frameOrder, value1, value2);
}
public ID3v2OutputFrame createCommentFrame(String value)
throws UnsupportedEncodingException, IOException
{
ID3FrameType frameType = ID3FrameType.COMMENT;
String longFrameID = frameType.longID;
Number frameOrder = frameType.getFrameOrder();
return toFrameCOMM(longFrameID, frameOrder, value);
}
public ID3v2OutputFrame createPictureFrame(ImageData imageData)
throws UnsupportedEncodingException, IOException
{
ID3FrameType frameType = ID3FrameType.PICTURE;
String longFrameID = frameType.longID;
Number frameOrder = frameType.getFrameOrder();
return toFrameImage(longFrameID, frameOrder, imageData);
}
public ID3v2OutputFrame createTextFrame(ID3FrameType frameType,
String value) throws UnsupportedEncodingException, IOException
{
return createTextFrame(frameType, value, null);
}
public ID3v2OutputFrame createTextFrame(ID3FrameType frameType,
String value1, String value2)
throws UnsupportedEncodingException, IOException
{
String longFrameID = frameType.longID;
Number frameOrder = frameType.getFrameOrder();
return toFrameText(longFrameID, frameOrder, value1, value2);
}
}
// TODO: convert Object params to String params.
private static ID3v2OutputFrame toFrameCOMM(String longFrameID,
Number frameOrder, String value)
throws UnsupportedEncodingException, IOException
{
String s;
if (value instanceof String)
s = (String) value;
else
{
Debug
.debug("Bad value ", value + " (" + Debug.getType(value)
+ ")");
Debug.dumpStack();
return null;
}
boolean use_iso = canEncodeStringInISO(s);
int char_encoding_code = use_iso ? CHAR_ENCODING_CODE_ISO_8859_1
: CHAR_ENCODING_CODE_UTF_16_WITH_BOM;
// : CHAR_ENCODING_CODE_UTF_8;
byte string_bytes[] = encodeString(s, use_iso);
// int frame_length = kFRAME_HEADER_LENGTH + string_bytes.length + 1;
int result_length = string_bytes.length + 1 + 3 + 1;
byte result[] = new byte[result_length];
int index = 0;
result[index++] = (byte) char_encoding_code;
result[index++] = (byte) 0; // language
result[index++] = (byte) 0; // language
result[index++] = (byte) 0; // language
// summary
result[index++] = (byte) 0; // divider
System.arraycopy(string_bytes, 0, result, index, string_bytes.length);
return new ID3v2OutputFrame(longFrameID, frameOrder, result);
}
private List toFrames(MyID3Listener listener, boolean strict,
IMusicMetadata metadata) throws UnsupportedEncodingException,
IOException, ID3WriteException
{
// make a local copy.
metadata = new MusicMetadata(metadata);
IFrameFactory frameFactory = new FrameFactory();
return ID3v2FrameTranslation.translateMetadataToFrames(listener,
strict, metadata, frameFactory);
}
private static final Comparator FRAME_SORTER = new Comparator() {
public int compare(Object o1, Object o2)
{
ID3v2OutputFrame f1 = (ID3v2OutputFrame) o1;
ID3v2OutputFrame f2 = (ID3v2OutputFrame) o2;
int fo1 = f1.frameOrder.intValue();
int fo2 = f2.frameOrder.intValue();
if (fo1 != fo2)
return fo1 - fo2;
return f1.longFrameID.compareTo(f2.longFrameID);
}
};
public interface Filter
{
public boolean filter(String frameid);
}
private byte[] writeFrames(Filter filter, List frames)
throws ID3WriteException, IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Collections.sort(frames, FRAME_SORTER);
for (int i = 0; i < frames.size(); i++)
{
ID3v2OutputFrame frame = (ID3v2OutputFrame) frames.get(i);
String frame_id = frame.longFrameID;
if (frame_id.length() != 4)
throw new ID3WriteException("frame_id has bad length: "
+ frame_id + " (" + frame_id.length() + ")");
if (filter != null && filter.filter(frame_id))
{
continue;
}
// baos.write(frame_id );
baos.write((byte) frame_id.charAt(0));
baos.write((byte) frame_id.charAt(1));
baos.write((byte) frame_id.charAt(2));
baos.write((byte) frame_id.charAt(3));
int length = frame.bytes.length;
if (id3v2_version == 4)
{
baos.write((byte) (0x7f & (length >> 21)));
baos.write((byte) (0x7f & (length >> 14)));
baos.write((byte) (0x7f & (length >> 7)));
baos.write((byte) (0x7f & (length)));
} else if (id3v2_version == 3)
{
baos.write((byte) (0xff & (length >> 24)));
baos.write((byte) (0xff & (length >> 16)));
baos.write((byte) (0xff & (length >> 8)));
baos.write((byte) (0xff & (length)));
} else
throw new ID3WriteException("id3v2_version: " + id3v2_version);
// int flags = frame.flags;
int flags = 0;
if (id3v2_version == 4)
{
if (frame.flags.getTagAlterPreservation())
flags |= FRAME_FLAG_ID3v24_TAG_ALTER_PRESERVATION;
if (frame.flags.getFileAlterPreservation())
flags |= FRAME_FLAG_ID3v24_FILE_ALTER_PRESERVATION;
if (frame.flags.getReadOnly())
flags |= FRAME_FLAG_ID3v24_READ_ONLY;
if (frame.flags.getGroupingIdentity())
flags |= FRAME_FLAG_ID3v24_GROUPING_IDENTITY;
if (frame.flags.getCompression())
flags |= FRAME_FLAG_ID3v24_COMPRESSION;
if (frame.flags.getEncryption())
flags |= FRAME_FLAG_ID3v24_ENCRYPTION;
if (frame.flags.getUnsynchronisation())
flags |= FRAME_FLAG_ID3v24_UNSYNCHRONISATION;
if (frame.flags.getDataLengthIndicator())
flags |= FRAME_FLAG_ID3v24_DATA_LENGTH_INDICATOR;
} else if (id3v2_version == 3)
{
if (frame.flags.getTagAlterPreservation())
flags |= FRAME_FLAG_ID3v23_TAG_ALTER_PRESERVATION;
if (frame.flags.getFileAlterPreservation())
flags |= FRAME_FLAG_ID3v23_FILE_ALTER_PRESERVATION;
if (frame.flags.getReadOnly())
flags |= FRAME_FLAG_ID3v23_READ_ONLY;
if (frame.flags.getGroupingIdentity())
flags |= FRAME_FLAG_ID3v23_GROUPING_IDENTITY;
if (frame.flags.getCompression())
flags |= FRAME_FLAG_ID3v23_COMPRESSION;
if (frame.flags.getEncryption())
flags |= FRAME_FLAG_ID3v23_ENCRYPTION;
} else
throw new ID3WriteException("id3v2_version: " + id3v2_version);
baos.write((byte) (0xff & (flags >> 8)));
baos.write((byte) (0xff & (flags)));
baos.write(frame.bytes);
}
return baos.toByteArray();
}
private void checkTags(MyID3Listener listener, MusicMetadataSet set,
List frames, boolean strict) throws UnsupportedEncodingException,
IOException, ID3WriteException
{
if (set == null || set.id3v2Raw == null)
return;
Vector old_frames = set.id3v2Raw.frames;
if (old_frames == null)
return;
Vector new_frame_ids = new Vector();
for (int i = 0; i < frames.size(); i++)
{
ID3v2OutputFrame frame = (ID3v2OutputFrame) frames.get(i);
new_frame_ids.add(frame.longFrameID);
}
Vector final_frame_ids = new Vector(new_frame_ids);
for (int i = 0; i < old_frames.size(); i++)
{
MyID3v2Frame oldFrame = (MyID3v2Frame) old_frames.get(i);
String longFrameID;
Number frameOrder;
{
ID3FrameType frame_type = ID3FrameType.get(oldFrame.frameID);
if (frame_type != null)
{
longFrameID = frame_type.longID;
frameOrder = frame_type.getFrameOrder();
} else if (oldFrame.frameID.length() == 4)
{
longFrameID = oldFrame.frameID;
frameOrder = ID3FrameType.DEFAULT_FRAME_ORDER;
} else
{
if (strict)
throw new ID3WriteException("unknown frame type: "
+ oldFrame.frameID);
else if (null != listener)
{
listener.log("unknown frame type", oldFrame.frameID);
listener.log("unknown old_tag", oldFrame);
listener.log(Debug.getStackTrace());
}
continue;
}
}
if (new_frame_ids.contains(longFrameID))
continue;
if (null != listener)
listener.log("adding missing frame", longFrameID);
if (oldFrame instanceof MyID3v2FrameText)
{
MyID3v2FrameText text_frame = (MyID3v2FrameText) oldFrame;
// ID3FrameType frame_type =
// ID3FrameType.get(old_frame.frame_id);
if (null != listener)
{
listener.log("text_frame", text_frame);
listener.log("frame_type", longFrameID);
}
ID3v2OutputFrame frame = toFrame(longFrameID, frameOrder,
text_frame.value, text_frame.value2);
if (frame != null)
{
frames.add(frame);
final_frame_ids.add(frame.longFrameID);
} else
{
if (strict)
throw new ID3WriteException("Couldn't write frame: "
+ longFrameID);
else if (null != listener)
{
listener.log("Couldn't write frame", longFrameID);
listener.log(Debug.getStackTrace());
}
}
} else if (oldFrame instanceof MyID3v2FrameImage)
{
MyID3v2FrameImage imageFrame = (MyID3v2FrameImage) oldFrame;
if (null != listener)
{
listener.log("imageFrame", imageFrame);
listener.log("frame_type", longFrameID);
}
ID3v2OutputFrame frame = toFrameImage(longFrameID, frameOrder,
imageFrame.getImageData());
frames.add(frame);
final_frame_ids.add(frame.longFrameID);
} else
{
MyID3v2FrameData data = (MyID3v2FrameData) oldFrame;
if (data.flags.getTagAlterPreservation())
continue;
// if(data.flags.getTagAlterPreservation())
// continue;
if (data.frameID.length() == 4)
{
// if(data.flags.getCompression() ||
// data.flags.getUnsynchronisation() || )
// int flags = data.flags.flags;
ID3v2OutputFrame frame = new ID3v2OutputFrame(data.frameID,
data.dataBytes, data.flags);
frames.add(frame);
final_frame_ids.add(frame.longFrameID);
continue;
}
if (strict)
throw new ID3WriteException(
"Couldn't preserve data frame: " + data.frameID);
else if (null != listener)
{
listener.log("Couldn't preserve data frame", data.frameID);
listener.log(Debug.getStackTrace());
}
}
// if()
}
// Debug.debug("final_frame_ids", final_frame_ids);
}
public byte[] toTag(MyID3Listener listener, MusicMetadataSet set,
IMusicMetadata values, boolean strict) throws Exception
{
return toTag(listener, null, set, values, strict);
}
public byte[] toTag(MyID3Listener listener, Filter filter,
MusicMetadataSet set, IMusicMetadata values, boolean strict)
throws UnsupportedEncodingException, IOException, ID3WriteException
{
// Debug.debug("raw values ", values);
List frames = toFrames(listener, strict, values);
// Debug.debug("raw frames", frames);
checkTags(listener, set, frames, strict);
if (null != listener)
{
for (int i = 0; i < frames.size(); i++)
{
ID3v2OutputFrame frame = (ID3v2OutputFrame) frames.get(i);
listener.log("frame", frame.longFrameID);
}
}
// Debug.debug("checked frames", frames);
byte frame_bytes[] = writeFrames(filter, frames);
// Debug.debug("frame_bytes", frame_bytes);
byte extended_header[] = {};
byte padding[] = {};
int body_length = extended_header.length + frame_bytes.length
+ padding.length;
byte header[] = getHeaderFooter(body_length, false);
// Debug.debug("body_length", body_length);
// Debug.debug("header", header);
byte footer[];
if (id3v2_version == 4)
footer = getHeaderFooter(body_length, true);
else if (id3v2_version == 3)
footer = null;
else
throw new ID3WriteException("id3v2_version: " + id3v2_version);
// Debug.debug("footer", footer);
int resultLength = header.length + extended_header.length
+ frame_bytes.length + padding.length;
if (footer != null)
resultLength += footer.length;
byte result[] = new byte[resultLength];
int index = 0;
System.arraycopy(header, 0, result, index, header.length);
index += header.length;
System.arraycopy(frame_bytes, 0, result, index, frame_bytes.length);
if (footer != null)
{
index += frame_bytes.length;
System.arraycopy(footer, 0, result, index, footer.length);
}
// index += footer.length;
return result;
}
}