/*
You may freely copy, distribute, modify and use this class as long
as the original author attribution remains intact. See message
below.
Copyright (C) 2001-2004 Christian Pesch. All Rights Reserved.
*/
package slash.metamusic.mp3;
import slash.metamusic.mp3.util.TimeConversion;
import slash.metamusic.util.Files;
import slash.metamusic.util.InputOutput;
import java.io.*;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.logging.Logger;
/**
* My instances represent an MP3 File, which is parsed. Then,
* information about the MP3 Header and the ID3 Tags may be
* queried.
*
* @author Christian Pesch
* @version $Id: MP3File.java 956 2007-02-03 10:39:39Z cpesch $
*/
public class MP3File implements ID3MetaData {
protected static final Logger log = Logger.getLogger(MP3File.class.getName());
private static final int READ_BUFFER_SIZE = 64 * 1024;
private File file;
private boolean valid = false;
private AbstractAudioProperties properties = new MP3Properties();
private ID3v1Tail tail = new ID3v1Tail();
private APETail ape = new APETail();
private ID3v2Header head = new ID3v2Header();
private ID3FileName fileName = new ID3FileName();
/**
* Returns the data file or null if we've read from a stream
*
* @return the data file or null if we've read from a stream
*/
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
/**
* Read given File and return an MP3File. If an error
* occurs or the file is invalid, null is returned.
*
* @param file the file to read
* @return an MP3File for the given File or null, if
* the file is invalid or an error occured
*/
public static MP3File readValidFile(File file) {
MP3File mp3 = new MP3File();
try {
if (mp3.read(file)) {
if (mp3.isValid()) {
return mp3;
}
}
} catch (IOException e) {
log.severe("Cannot process invalid MP3 file " + file.getAbsolutePath() + ": " + e.getMessage());
}
return null;
}
/**
* Read the file for the given file name.
*
* @param fileName the fileName to read a file from
* @return true, if the was could be read
* @throws IOException if an error occured reading the fine
*/
public boolean read(String fileName) throws IOException {
return read(new File(Files.replaceSeparators(fileName)));
}
/**
* Read the given file.
*
* @param file the file to read
* @return true, if the was could be read
* @throws IOException if an error occured reading the fine
*/
public boolean read(File file) throws IOException {
if (!file.exists())
throw new IOException("File " + file.getAbsolutePath() + " does not exist");
if (!file.isFile())
throw new IOException("File " + file.getAbsolutePath() + " is not a file");
this.file = file;
valid = true;
log.info("Analyzing " + getFile().getAbsolutePath() + " (" + file.length() + " bytes)");
fileName = new ID3FileName();
valid = valid & fileName.read(file);
if (file.getName().toLowerCase().endsWith(".mp3"))
properties = new MP3Properties();
else if (file.getName().toLowerCase().endsWith(".wav"))
properties = new WAVProperties();
else if (file.getName().toLowerCase().endsWith(".ogg"))
properties = new OggProperties();
FileInputStream in = new FileInputStream(file);
try {
valid = valid & read(in);
} finally {
in.close();
}
if (properties.getFileSize() != 0 && getFileSize() != properties.getFileSize()) {
log.severe("File size differs: " + getFileSize() + " bytes calculated, but " +
properties.getFileSize() + " bytes in header (difference " +
(getFileSize() - properties.getFileSize()) + ")");
}
properties.setFileSize(getFileSize());
return valid;
}
public boolean read(InputStream in) throws IOException {
valid = true;
InputStream buffer;
if (!in.markSupported()) {
buffer = new BufferedInputStream(in, READ_BUFFER_SIZE);
} else
buffer = in;
try {
buffer.mark(READ_BUFFER_SIZE);
readHead(buffer);
buffer.mark(READ_BUFFER_SIZE);
readProperties(buffer);
// fast forward for ID3v1Tail searching
int available = in.available();
if (available > ID3v1Tail.ID3V1_SIZE * 3) {
int toSkip = available - ID3v1Tail.ID3V1_SIZE * 3;
long skipped = in.skip(toSkip);
if (skipped < toSkip)
log.fine("Skipped " + skipped + " but wanted to skip " + toSkip + " bytes");
}
buffer.mark(READ_BUFFER_SIZE);
readTail(buffer, READ_BUFFER_SIZE);
properties.setMetaDataSize(getReadSize());
} finally {
buffer.close();
}
return valid;
}
private void readHead(InputStream buffer) throws IOException {
head = new ID3v2Header();
try {
head.read(buffer);
} catch (NoID3v2HeaderException e) {
log.info("No ID3v2 head found");
buffer.reset();
}
}
private void readProperties(InputStream buffer) throws IOException {
try {
valid = properties.read(buffer);
} catch (NoMP3FrameException e) {
log.severe("No valid properties found");
try {
buffer.reset();
}
catch (IOException e2) {
// means that a valid MP3 frame was not found within READ_BUFFER_SIZE
}
valid = false;
}
}
private void readTail(InputStream buffer, int bufferSize) throws IOException {
ape = new APETail();
try {
ape.read(buffer, bufferSize);
} catch (NoAPEv2TailException e) {
log.fine("No APEv2 tail found");
buffer.reset();
}
tail = new ID3v1Tail();
try {
tail.read(buffer);
} catch (NoID3v1TailException e) {
log.info("No ID3v1 tail found");
}
}
public void write(File file) throws IOException {
if (!file.isFile())
throw new IOException("File " + file.getAbsolutePath() + " is not a file");
if (!file.canWrite())
throw new IOException("No permission to write on file " + file.getAbsolutePath());
this.file = file;
File tmp = File.createTempFile("mp3file", ".mp3");
OutputStream out = new FileOutputStream(tmp);
try {
write(out);
} finally {
out.close();
}
// not required on Unix, but on Windows we need to delete the target before
// renaming another file to it or copying bytes to it
if (!file.delete())
throw new IOException("Cannote delete target file " + file.getAbsolutePath());
// temp file: rename file to original filename
// if temp file and file are in the same directory, we can rename
File tmp1 = new File(tmp.getAbsolutePath());
File tmp2 = new File(file.getAbsolutePath());
if (tmp1.getParent().equals(tmp2.getParent())) {
if (!tmp.renameTo(file))
throw new IOException("Cannote rename " + tmp.getAbsolutePath() + " to " + file.getAbsolutePath());
} else {
// else, we must copy
InputStream in = new FileInputStream(tmp);
out = new FileOutputStream(file);
InputOutput inout = new InputOutput(in, out);
inout.start();
inout.close();
if (file.length() != tmp.length())
throw new IOException("Failed to copy " + tmp.getAbsolutePath() + " to " + file.getAbsolutePath() + " source: " + tmp.length() + " destination: " + file.length() + " bytes");
if (!tmp.delete())
throw new IOException("Cannote delete temporary file " + tmp.getAbsolutePath());
}
log.info("Wrote " + file.getAbsolutePath() + " (" + file.length() + " bytes)");
}
public void rename(File file) throws IOException {
copyID3MetaData(this, getFileName());
this.file = fileName.rename(file);
}
public void move(File file) throws IOException {
copyID3MetaData(this, getFileName());
this.file = fileName.move(file);
}
public void removeID3(File file) throws IOException {
setID3v1(false);
setID3v2(false);
write(file);
}
public void write(OutputStream out) throws IOException {
if (isID3v2())
head.write(out);
if (getFile() != null) {
FileInputStream in = new FileInputStream(getFile());
// skip header
if (in.skip(head.getReadSize()) != head.getReadSize())
log.warning("Could skip " + properties.getReadSize() + " bytes of header");
// skip padding
if (in.skip(properties.getReadSize()) != properties.getReadSize())
log.warning("Could skip " + properties.getReadSize() + " bytes of padding");
// copy body
InputOutput inout = new InputOutput(in, out);
long bytes = getFileSize() - head.getReadSize() - properties.getReadSize() - tail.getReadSize();
log.fine("Writing MP3 data (" + bytes + " bytes)");
inout.copy(bytes);
in.close();
}
if (isID3v1())
tail.write(out);
}
public void write() throws IOException {
write(getFile());
}
private void copyID3MetaData(ID3MetaData from, ID3MetaData to) {
to.setArtist(from.getArtist());
to.setComment(from.getComment());
to.setGenre(from.getGenre());
to.setAlbum(from.getAlbum());
to.setTrack(from.getTrack());
try {
to.setIndex(from.getIndex());
}
catch (IllegalArgumentException e) {
// happens if a IDv1 but not 1.1 tail is asked for an index
}
to.setYear(from.getYear());
}
// --- get object ------------------------------------------
/**
* Returns the data file size or -1 if we've read from a stream
*
* @return the data file size or -1 if we've read from a stream
*/
public long getFileSize() {
return getFile() != null ? getFile().length() : -1;
}
public AbstractAudioProperties getProperties() {
return properties;
}
public APETail getApe() {
return ape;
}
public ID3v1Tail getTail() {
return tail;
}
public ID3v2Header getHead() {
return head;
}
public ID3FileName getFileName() {
return fileName;
}
public boolean isMP3() {
return properties.isMP3();
}
public boolean isWAV() {
return properties.isWAV();
}
public boolean isOgg() {
return properties.isOgg();
}
public boolean isAPE() {
return ape.isValid();
}
public boolean isID3v1() {
return tail.isValid();
}
public boolean isID3v1dot1() {
return isID3v1() && tail.isID3v1dot1();
}
public boolean isID3v2() {
return head.isValid();
}
public long getBitRate() {
return properties.getBitRate();
}
public long getSampleFrequency() {
return properties.getSampleFrequency();
}
public boolean isVBR() {
return properties.isVBR();
}
public int getMode() {
return properties.getMode();
}
public String getModeAsString() {
return properties.getModeAsString();
}
public int getSeconds() {
if (isID3v2()) {
if (properties.getSeconds() != -1 && head.getSeconds() != -1 &&
properties.getSeconds() != head.getSeconds()) {
log.warning("Inconsistent seconds, head=" + head.getSeconds() + " properties=" + properties.getSeconds());
}
}
return properties.getSeconds();
}
public String getSecondsAsTimeString() {
return TimeConversion.getTimeFromSeconds(getSeconds());
}
// --- MetaData get ----------------------------------------
/**
* Returns if the MP3 meta data has been read successfully.
*
* @return true if the MP3 meta data has been read successfully
*/
public boolean isValid() {
return valid;
}
public long getReadSize() {
return tail.getReadSize() + properties.getReadSize() + head.getReadSize();
}
private String calculateValueFromHeadOrTail(String headValue, String tailValue) {
if (headValue != null && tailValue != null &&
(!headValue.startsWith(tailValue))) {
log.severe("Inconsistent value, head=" + headValue + " tail=" + tailValue);
}
String currentValue = tailValue;
// if the tail defines a substring of the head, take the head
if (currentValue == null || currentValue.length() == 0 || currentValue.indexOf('\u0000') != -1 ||
(headValue != null && headValue.startsWith(currentValue)))
currentValue = headValue;
return currentValue;
}
public String getTrack() {
String trackName = null;
if (isID3v1())
trackName = tail.getTrack();
if (isID3v2())
trackName = calculateValueFromHeadOrTail(head.getTrack(), trackName);
if (trackName /*still*/ == null)
trackName = fileName.getTrack();
return trackName;
}
public String getArtist() {
String artistName = null;
if (isID3v1())
artistName = tail.getArtist();
if (isID3v2())
artistName = calculateValueFromHeadOrTail(head.getArtist(), artistName);
if (artistName /*still*/ == null)
artistName = fileName.getArtist();
return artistName;
}
public String getAlbum() {
String albumName = null;
if (isID3v1())
albumName = tail.getAlbum();
if (isID3v2())
albumName = calculateValueFromHeadOrTail(head.getAlbum(), albumName);
if (albumName /*still*/ == null)
albumName = fileName.getAlbum();
return albumName;
}
public ID3Genre getGenre() {
ID3Genre genre = null;
if (isID3v1()) {
genre = tail.getGenre();
if (genre != null && genre.getId() == -1)
genre = null;
}
if (isID3v2()) {
if (head.getGenre() != null && genre != null &&
!head.getGenre().equals(genre)) {
log.severe("Inconsistent genre, head=" + head.getGenre() +
" tail=" + tail.getGenre());
}
if (genre == null)
genre = head.getGenre();
}
return genre;
}
public int getYear() {
int year = -1;
if (isID3v1())
year = tail.getYear();
if (isID3v2()) {
if (head.getYear() != -1 && year != -1 &&
head.getYear() != year) {
log.severe("Inconsistent year, head=" + head.getYear() +
" tail=" + tail.getYear());
}
if (year == -1)
year = head.getYear();
}
return year;
}
public String getComment() {
String comment = null;
if (isID3v1())
comment = tail.getComment();
if (isID3v2())
comment = calculateValueFromHeadOrTail(head.getComment(), comment);
return comment;
}
public int getIndex() {
int track = -1;
if (isID3v1dot1())
track = tail.getIndex();
if (isID3v2()) {
if (head.getIndex() != -1 && isID3v1dot1() && tail.getIndex() != -1 &&
head.getIndex() != tail.getIndex()) {
log.severe("Inconsistent track, head=" + head.getIndex() +
" tail=" + tail.getIndex());
}
if (track == -1)
track = head.getIndex();
}
if (track == -1)
track /*still*/ = fileName.getIndex();
return track;
}
public int getCount() {
return head.getCount();
}
public int getPartOfSetIndex() {
return head.getPartOfSetIndex();
}
public int getPartOfSetCount() {
return head.getPartOfSetCount();
}
public String getEncoder() {
String encoder = null;
if (isMP3())
encoder = ((MP3Properties) properties).getEncoder();
if (isID3v2())
encoder = calculateValueFromHeadOrTail(head.getEncoder(), encoder);
return encoder;
}
// --- MetaData set ----------------------------------------
public void setID3v1(boolean isID3v1) {
if (isID3v1 && !tail.isValid()) {
if (head.isValid())
copyID3MetaData(head, tail);
else if (fileName.isValid())
copyID3MetaData(fileName, tail);
}
tail.setValid(isID3v1);
}
public void setID3v2(boolean isID3v2) {
if (isID3v2 && !head.isValid()) {
if (tail.isValid())
copyID3MetaData(tail, head);
else if (fileName.isValid())
copyID3MetaData(fileName, head);
}
head.setValid(isID3v2);
}
public ID3v2Frame addID3v2Frame(String tagName) {
return head.addID3v2Frame(tagName);
}
public void setTrack(String newTrack) {
head.setTrack(newTrack);
tail.setTrack(newTrack);
}
public void setArtist(String newArtist) {
head.setArtist(newArtist);
tail.setArtist(newArtist);
}
public void setAlbum(String newAlbum) {
head.setAlbum(newAlbum);
tail.setAlbum(newAlbum);
}
public void setYear(int newYear) {
head.setYear(newYear);
tail.setYear(newYear);
}
public void setGenre(ID3Genre newGenre) {
head.setGenre(newGenre);
tail.setGenre(newGenre);
}
public void setIndex(int newIndex) {
head.setIndex(newIndex);
tail.setIndex(newIndex);
}
public void setCount(int newCount) {
head.setCount(newCount);
}
public void setPartOfSetIndex(int newIndex) {
head.setPartOfSetIndex(newIndex);
}
public void setPartOfSetCount(int newCount) {
head.setPartOfSetCount(newCount);
}
public void setComment(String newComment) {
head.setComment(newComment);
tail.setComment(newComment);
}
public void setSeconds(int newSeconds) {
head.setSeconds(newSeconds);
}
public void setMetaMusicComment() {
Calendar date = Calendar.getInstance();
DateFormat fullFormat = DateFormat.getDateTimeInstance();
getHead().setComment("Written by MetaMusic on " + fullFormat.format(date.getTime()), "Written", "English");
getHead().setTaggingTime(date);
DateFormat shortFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
getTail().setComment("MetaMusic on " + shortFormat.format(date.getTime()));
// normalizes genre tag to "name(id)" if it is not "Unknown(-1)"
ID3Genre genre = getGenre();
if (genre != null && genre.getName() != null && !genre.getName().equals(ID3Genre.UNKNOWN)) {
getHead().setGenre(genre);
getTail().setGenre(genre);
}
// sets track count if track index and count exist
int count = getHead().getCount();
if (count != -1)
getHead().setCount(count);
int seconds = getProperties().getSeconds();
if (seconds != -1)
getHead().setSeconds(seconds);
}
// --- overwrites Object -----------------------------------
public String toString() {
StringBuilder buffer = new StringBuilder();
if (getFile() != null) {
buffer.append("file: ").append(getFile().getAbsolutePath()).append("\n").
append("size: ").append(getFileSize()).append(" bytes\n").
append("valid: ").append(isValid());
}
if (!fileName.isValid())
buffer.append("valid name: ").append(fileName.isValid()).append("\n");
if (isValid()) {
buffer.append("vbr: ").append(isVBR()).append("\n").
append("bitrate: ").append(getBitRate()).append(" bit\\s\n").
append("sample freq: ").append(getSampleFrequency()).append(" Hz\n").
append("mode: ").append(getModeAsString()).append(" (").append(getMode()).append(")\n").
append("time: ").append(getSecondsAsTimeString()).append(" (").append(getSeconds()).append(" secs)\n");
if (properties instanceof MP3Properties) {
MP3Properties mp3 = (MP3Properties) properties;
buffer.append("frames: ").append(mp3.getFrames()).append("\n").
append("frame size: ").append(mp3.getFrameSize()).append("\n").
append("version: ").append(mp3.getMPEGVersionString()).append(" (").append(mp3.getMPEGVersion()).append(")\n").
append("layer: ").append(mp3.getMPEGLayerString()).append("\n").
append("padding: ").append(mp3.getPadding()).append("\n").
append("protection: ").append(mp3.isProtected()).append("\n");
if (mp3.isProtected())
buffer.append("crc: ").append(mp3.getCRC()).append("\n");
buffer.append("mode ext: ").append(mp3.getModeExtension()).append("\n").
append("private: ").append(mp3.isPrivate()).append("\n").
append("copyrighted: ").append(mp3.isCopyrighted()).append("\n").
append("original: ").append(mp3.isOriginal()).append("\n").
append("emphasis: ").append(mp3.getEmphasisString()).append(" (").append(mp3.getEmphasis()).append(")\n");
if (mp3.getEncoder().length() > 0) {
buffer.append("encoder: ").append(getEncoder()).append("\n");
}
buffer.append("valid mp3: ").append(mp3.isValid()).append("\n");
}
if (properties instanceof WAVProperties) {
WAVProperties wav = (WAVProperties) properties;
buffer.append("bits/sample: ").append(wav.getBitsPerSample()).append("\n");
buffer.append("valid wav: ").append(wav.isValid()).append("\n");
}
if (properties instanceof OggProperties) {
OggProperties ogg = (OggProperties) properties;
ogg.getBitRate(); // TODO OggProperties should be extended
buffer.append("valid ogg: ").append(ogg.isValid()).append("\n");
}
buffer.append("APE: ").append(isAPE()).append("\n");
if (isAPE()) {
buffer.append("APE rel: ").append(ape.getVersion()).append("\n");
}
buffer.append("ID3v1: ").append(isID3v1()).append("\n");
if (isID3v1())
buffer.append("ID3v1.1: ").append(isID3v1dot1()).append("\n");
buffer.append("ID3v2: ").append(isID3v2()).append("\n");
if (isID3v2()) {
buffer.append("ID3v2 rel: ").append(head.getVersion().getVersionString()).append("\n");
for (ID3v2Frame f : head.getFrames()) {
buffer.append(f.getTagName());
String description = f.getTagDescription();
if (description != null) {
buffer.append(" [").append(description).append("]");
}
String stringContent = f.getStringContent();
if (stringContent == null || stringContent.length() < 1000)
buffer.append(": ").append(stringContent).append("\n");
else {
buffer.append(": [").append(stringContent.length()).append(" bytes]\n");
}
}
}
buffer.append("track: ").append(getTrack()).append("\n").
append("artist: ").append(getArtist()).append("\n").
append("album: ").append(getAlbum()).append("\n");
if (isID3v1() || isID3v2()) {
buffer.append("year: ").append(getYear()).append("\n").
append("comment: ").append(getComment()).append("\n");
if (isID3v1dot1() || isID3v2() || fileName.getIndex() != -1)
buffer.append("index: ").append(getIndex()).append("\n");
if (isID3v2() && getCount() != -1)
buffer.append("count: ").append(getCount()).append("\n");
buffer.append("genre: ").append(getGenre()).append("\n");
if (isID3v2()) {
int rating = head.getRating();
if (rating > 0)
buffer.append("rating: ").append(rating).append("\n");
int playCount = head.getPlayCount();
if (playCount > 0)
buffer.append("play count: ").append(playCount).append("\n");
Calendar playTime = head.getPlayTime();
if (playTime != null) {
String playTimeStr = DateFormat.getDateTimeInstance().format(playTime.getTime());
buffer.append("play time: ").append(playTimeStr).append("\n");
}
}
}
}
return buffer.toString();
}
}