package org.farng.mp3.id3;
import org.farng.mp3.AbstractMP3Tag;
import org.farng.mp3.TagNotFoundException;
import org.farng.mp3.TagOptionSingleton;
import org.farng.mp3.TagUtility;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Iterator;
/**
* <TABLE border=0> <TBODY> <TR> <TD class=h2>What is ID3 (v1)?</TD></TR></TBODY></TABLE> <TABLE border=0> <TBODY> <TR
* vAlign=top> <TD> <P>The audio format MPEG layer I, layer II and layer III (MP3) has no native way of saving
* information about the contents, except for some simple yes/no parameters like "private", "copyrighted" and "original
* home" (meaning this is the original file and not a copy). A solution to this problem was introduced with the program
* "Studio3" by Eric Kemp alias NamkraD in 1996. By adding a small chunk of extra data in the end of the file one could
* get the MP3 file to carry information about the audio and not just the audio itself.</P>
* <p/>
* <P>The placement of the tag, as the data was called, was probably chosen as there were little chance that it should
* disturb decoders. In order to make it easy to detect a fixed size of 128 bytes was chosen. The tag has the following
* layout (as hinted by the scheme to the right):</P> <CENTER>
* <p/>
* <TABLE cellSpacing=0 cellPadding=2 border=1> <TBODY> <TR> <TD>Song title</TD> <TD>30 characters</TD></TR> <TR>
* <TD>Artist</TD> <TD>30 characters</TD></TR> <TR> <TD>Album</TD> <TD>30 characters</TD></TR> <TR> <TD>Year</TD> <TD>4
* characters</TD></TR> <TR> <TD>Comment</TD> <TD>30 characters</TD></TR> <TR> <TD>Genre</TD> <TD>1
* byte</TD></TR></TBODY></TABLE> </P></CENTER>
* <p/>
* <P class=t>If you one sum the the size of all these fields we see that 30+30+30+4+30+1 equals 125 bytes and not 128
* bytes. The missing three bytes can be found at the very end of the tag, before the song title. These three bytes are
* always "TAG" and is the identification that this is indeed a ID3 tag. The easiest way to find a ID3v1/1.1 tag is to
* look for the word "TAG" 128 bytes from the end of a file.</P>
* <p/>
* <P class=t>As all artists doesn't have a 30 character name it is said that if there is some bytes left after the
* information is entered in the field, those bytes should be fille with the binary value 0. You might also think that
* you cannot write that much in the genre field, being one byte big, but it is more clever than that. The byte value
* you enter in the genre field corresponds to a value in a predefined list. The list that Eric Kemp created had 80
* entries, ranging from 0 to 79.</P></TD> </TR></TBODY></TABLE>
*
* @author Eric Farng
* @version $Revision: 1.6 $
*/
public class ID3v1 extends AbstractID3v1 {
protected String album = "";
protected String artist = "";
protected String comment = "";
protected String title = "";
protected String year = "";
protected byte genre = -1;
/**
* Creates a new ID3v1 object.
*/
public ID3v1() {
// base empty constructor
}
/**
* Creates a new ID3v1 object.
*/
public ID3v1(final ID3v1 copyObject) {
super(copyObject);
this.album = new String(copyObject.album);
this.artist = new String(copyObject.artist);
this.comment = new String(copyObject.comment);
this.title = new String(copyObject.title);
this.year = new String(copyObject.year);
this.genre = copyObject.genre;
}
/**
* Creates a new ID3v1 object.
*/
public ID3v1(final AbstractMP3Tag mp3tag) {
if (mp3tag != null) {
final ID3v1_1 convertedTag;
if (mp3tag instanceof ID3v1 && !(mp3tag instanceof ID3v1_1)) {
throw new UnsupportedOperationException("Copy Constructor not called. Please type cast the argument");
}
if (mp3tag instanceof ID3v1_1) {
convertedTag = (ID3v1_1) mp3tag;
} else {
convertedTag = new ID3v1_1(mp3tag);
}
this.album = new String(convertedTag.album.trim());
this.artist = new String(convertedTag.artist.trim());
this.comment = new String(convertedTag.comment.trim());
this.title = new String(convertedTag.title.trim());
this.year = new String(convertedTag.year.trim());
this.genre = convertedTag.genre;
}
}
/**
* Creates a new ID3v1 object.
*/
public ID3v1(final RandomAccessFile file) throws TagNotFoundException, IOException {
this.read(file);
}
public void setAlbum(final String album) {
this.album = TagUtility.truncate(album, 30);
}
public String getAlbum() {
return this.album;
}
public void setArtist(final String artist) {
this.artist = TagUtility.truncate(artist, 30);
}
public String getArtist() {
return this.artist;
}
public void setComment(final String comment) {
this.comment = TagUtility.truncate(comment, 30);
}
public String getComment() {
return this.comment;
}
public void setGenre(final byte genre) {
this.genre = genre;
}
public byte getGenre() {
return this.genre;
}
public ID3v1 getID3tag(final RandomAccessFile file) throws IOException {
ID3v1 id3v1tag = new ID3v1_1();
// look for id3v1_1 tag
if (id3v1tag.seek(file) == true) {
try {
id3v1tag.read(file);
id3v1tag.delete(file);
} catch (TagNotFoundException ex) {
id3v1tag = null;
}
} else {
id3v1tag = null;
}
if (id3v1tag == null) {
// look for id3v1 tag
id3v1tag = new ID3v1();
if (id3v1tag.seek(file) == true) {
try {
id3v1tag.read(file);
id3v1tag.delete(file);
} catch (TagNotFoundException ex) {
id3v1tag = null;
}
} else {
id3v1tag = null;
}
}
return id3v1tag;
}
public String getIdentifier() {
return "ID3v1.00";
}
public int getSize() {
return 128;
}
public void setTitle(final String title) {
this.title = TagUtility.truncate(title, 30);
}
public String getTitle() {
return this.title;
}
public void setYear(final String year) {
this.year = TagUtility.truncate(year, 4);
}
public String getYear() {
return this.year;
}
public void append(final AbstractMP3Tag tag) {
final ID3v1 oldTag = this;
final ID3v1 newTag;
if (tag != null) {
if (tag instanceof ID3v1) {
newTag = (ID3v1) tag;
} else {
newTag = new ID3v1();
}
if (tag instanceof org.farng.mp3.lyrics3.AbstractLyrics3) {
TagOptionSingleton.getInstance().setId3v1SaveYear(false);
TagOptionSingleton.getInstance().setId3v1SaveComment(false);
}
oldTag.title = (TagOptionSingleton.getInstance().isId3v1SaveTitle() && (oldTag.title.length() == 0)) ?
newTag.title :
oldTag.title;
oldTag.artist = (TagOptionSingleton.getInstance().isId3v1SaveArtist() && (oldTag.artist.length() == 0)) ?
newTag.artist :
oldTag.artist;
oldTag.album = (TagOptionSingleton.getInstance().isId3v1SaveAlbum() && (oldTag.album.length() == 0)) ?
newTag.album :
oldTag.album;
oldTag.year = (TagOptionSingleton.getInstance().isId3v1SaveYear() && (oldTag.year.length() == 0)) ?
newTag.year :
oldTag.year;
oldTag.comment = (TagOptionSingleton.getInstance().isId3v1SaveComment() && (oldTag.comment.length() == 0)) ?
newTag.comment :
oldTag.comment;
oldTag.genre = (TagOptionSingleton.getInstance().isId3v1SaveGenre() && (oldTag.genre < 0)) ?
newTag.genre :
oldTag.genre;
// we don't need to reset the tag options because
// we want to save all fields (default)
}
}
public void delete(final RandomAccessFile file) throws IOException {
if (seek(file)) {
file.setLength(file.length() - 128);
}
}
public boolean equals(final Object obj) {
if ((obj instanceof ID3v1) == false) {
return false;
}
final ID3v1 id3v1 = (ID3v1) obj;
if (this.album.equals(id3v1.album) == false) {
return false;
}
if (this.artist.equals(id3v1.artist) == false) {
return false;
}
if (this.comment.equals(id3v1.comment) == false) {
return false;
}
if (this.genre != id3v1.genre) {
return false;
}
if (this.title.equals(id3v1.title) == false) {
return false;
}
if (this.year.equals(id3v1.year) == false) {
return false;
}
return super.equals(obj);
}
public Iterator iterator() {
return new ID3v1Iterator(this);
}
public void overwrite(final AbstractMP3Tag tag) {
final ID3v1 oldTag = this;
final ID3v1 newTag;
if (tag != null) {
if (tag instanceof ID3v1) {
newTag = (ID3v1) tag;
} else {
newTag = new ID3v1();
}
if (tag instanceof org.farng.mp3.lyrics3.AbstractLyrics3) {
TagOptionSingleton.getInstance().setId3v1SaveYear(false);
TagOptionSingleton.getInstance().setId3v1SaveComment(false);
}
oldTag.title = TagOptionSingleton.getInstance().isId3v1SaveTitle() ? newTag.title : oldTag.artist;
oldTag.artist = TagOptionSingleton.getInstance().isId3v1SaveArtist() ? newTag.artist : oldTag.artist;
oldTag.album = TagOptionSingleton.getInstance().isId3v1SaveAlbum() ? newTag.album : oldTag.album;
oldTag.year = TagOptionSingleton.getInstance().isId3v1SaveYear() ? newTag.year : oldTag.year;
oldTag.comment = TagOptionSingleton.getInstance().isId3v1SaveComment() ? newTag.comment : oldTag.comment;
oldTag.genre = TagOptionSingleton.getInstance().isId3v1SaveGenre() ? newTag.genre : oldTag.genre;
// we don't need to reset the tag options because
// we want to save all fields (default)
}
}
public void read(final RandomAccessFile file) throws TagNotFoundException, IOException {
final byte[] buffer = new byte[30];
if (seek(file) == false) {
throw new TagNotFoundException("ID3v1 tag not found");
}
file.read(buffer, 0, 30);
this.title = new String(buffer, 0, 30, "ISO-8859-1").trim();
file.read(buffer, 0, 30);
this.artist = new String(buffer, 0, 30, "ISO-8859-1").trim();
file.read(buffer, 0, 30);
this.album = new String(buffer, 0, 30, "ISO-8859-1").trim();
file.read(buffer, 0, 4);
this.year = new String(buffer, 0, 4, "ISO-8859-1").trim();
file.read(buffer, 0, 30);
this.comment = new String(buffer, 0, 30, "ISO-8859-1").trim();
file.read(buffer, 0, 1);
this.genre = buffer[0];
}
public boolean seek(final RandomAccessFile file) throws IOException {
final byte[] buffer = new byte[3];
// If there's a tag, it's 127 bytes long and we'll find the tag
file.seek(file.length() - 128);
// read the TAG value
file.read(buffer, 0, 3);
final String tag = new String(buffer, 0, 3);
return tag.equals("TAG");
}
public String toString() {
String str = getIdentifier() + " " + this.getSize() + "\n";
str += ("Title = " + this.title + "\n");
str += ("Artist = " + this.artist + "\n");
str += ("Album = " + this.album + "\n");
str += ("Comment = " + this.comment + "\n");
str += ("Year = " + this.year + "\n");
str += ("Genre = " + this.genre + "\n");
return str;
}
public void write(final AbstractMP3Tag tag) {
final ID3v1 oldTag = this;
final ID3v1 newTag;
if (tag != null) {
if (tag instanceof ID3v1) {
newTag = (ID3v1) tag;
} else {
newTag = new ID3v1_1(tag);
}
oldTag.title = newTag.title;
oldTag.artist = newTag.artist;
oldTag.album = newTag.album;
oldTag.year = newTag.year;
oldTag.comment = newTag.comment;
oldTag.genre = newTag.genre;
}
}
public void write(final RandomAccessFile file) throws IOException {
final byte[] buffer = new byte[128];
int i;
int offset = 3;
String str;
delete(file);
file.seek(file.length());
buffer[0] = (byte) 'T';
buffer[1] = (byte) 'A';
buffer[2] = (byte) 'G';
if (TagOptionSingleton.getInstance().isId3v1SaveTitle()) {
str = TagUtility.truncate(this.title, 30);
for (i = 0; i < str.length(); i++) {
buffer[i + offset] = (byte) str.charAt(i);
}
}
offset += 30;
if (TagOptionSingleton.getInstance().isId3v1SaveArtist()) {
str = TagUtility.truncate(this.artist, 30);
for (i = 0; i < str.length(); i++) {
buffer[i + offset] = (byte) str.charAt(i);
}
}
offset += 30;
if (TagOptionSingleton.getInstance().isId3v1SaveAlbum()) {
str = TagUtility.truncate(this.album, 30);
for (i = 0; i < str.length(); i++) {
buffer[i + offset] = (byte) str.charAt(i);
}
}
offset += 30;
if (TagOptionSingleton.getInstance().isId3v1SaveYear()) {
str = TagUtility.truncate(this.year, 4);
for (i = 0; i < str.length(); i++) {
buffer[i + offset] = (byte) str.charAt(i);
}
}
offset += 4;
if (TagOptionSingleton.getInstance().isId3v1SaveComment()) {
str = TagUtility.truncate(this.comment, 30);
for (i = 0; i < str.length(); i++) {
buffer[i + offset] = (byte) str.charAt(i);
}
}
offset += 30;
if (TagOptionSingleton.getInstance().isId3v1SaveGenre()) {
buffer[offset] = this.genre;
}
file.write(buffer);
}
public String getSongTitle() {
return getTitle().trim();
}
public String getLeadArtist() {
return getArtist().trim();
}
public String getAlbumTitle() {
return getAlbum().trim();
}
public String getYearReleased() {
return getYear().trim();
}
public String getSongComment() {
return getComment().trim();
}
public String getSongGenre() {
return Integer.toString(getGenre());
}
public String getTrackNumberOnAlbum() {
throw new UnsupportedOperationException("This tag does not contain that information");
}
public String getSongLyric() {
throw new UnsupportedOperationException("This tag does not contain that information");
}
public String getAuthorComposer() {
throw new UnsupportedOperationException("This tag does not contain that information");
}
public void setSongTitle(String songTitle) {
setTitle(songTitle.trim());
}
public void setLeadArtist(String leadArtist) {
setArtist(leadArtist.trim());
}
public void setAlbumTitle(String albumTitle) {
setAlbum(albumTitle.trim());
}
public void setYearReleased(String yearReleased) {
setYear(yearReleased.trim());
}
public void setSongComment(String songComment) {
setComment(songComment.trim());
}
public void setSongGenre(String songGenre) {
setGenre(Byte.parseByte(songGenre.trim()));
}
public void setTrackNumberOnAlbum(String trackNumberOnAlbum) {
throw new UnsupportedOperationException("This tag does not contain that information");
}
public void setSongLyric(String songLyrics) {
throw new UnsupportedOperationException("This tag does not contain that information");
}
public void setAuthorComposer(String authorComposer) {
throw new UnsupportedOperationException("This tag does not contain that information");
}
}