Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>2.3.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/com/mpatric/mp3agic/FileInputSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.mpatric.mp3agic;

import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

public class FileInputSource implements InputSource {

private final Path path;

public FileInputSource(Path path) {
this.path = path;
}

@Override
public String getResourceName() {
return path.toString();
}

@Override
public boolean isReadable() {
return Files.exists(path) && Files.isReadable(path);
}

@Override
public long getLastModified() throws IOException {
return Files.getLastModifiedTime(path).to(TimeUnit.MILLISECONDS);
}

@Override
public long getLength() throws IOException {
return Files.size(path);
}

@Override
public SeekableByteChannel openChannel(OpenOption... options) throws IOException {
return Files.newByteChannel(path, options);
}

}
36 changes: 20 additions & 16 deletions src/main/java/com/mpatric/mp3agic/FileWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,57 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;

public class FileWrapper {

protected Path path;
protected long length;
protected InputSource inputSource;
protected long lastModified;

protected FileWrapper() {
}

public FileWrapper(String filename) throws IOException {
this.path = Paths.get(filename);
if (filename == null) throw new IllegalArgumentException("filename must not be null");
this.inputSource = new FileInputSource(Paths.get(filename));
init();
}

public FileWrapper(File file) throws IOException {
if (file == null) throw new NullPointerException();
this.path = Paths.get(file.getPath());
if (file == null) throw new IllegalArgumentException("file must not be null");
this.inputSource = new FileInputSource(Paths.get(file.getPath()));
init();
}

public FileWrapper(Path path) throws IOException {
if (path == null) throw new NullPointerException();
this.path = path;
if (path == null) throw new IllegalArgumentException("path must not be null");
this.inputSource = new FileInputSource(path);
init();
}

private void init() throws IOException {
if (!Files.exists(path)) throw new FileNotFoundException("File not found " + path);
if (!Files.isReadable(path)) throw new IOException("File not readable");
length = Files.size(path);
lastModified = Files.getLastModifiedTime(path).to(TimeUnit.MILLISECONDS);
public FileWrapper(InputSource inputSource) throws IOException {
if (inputSource == null) throw new IllegalArgumentException("inputSource must not be null");
this.inputSource = inputSource;
init();
}

public String getFilename() {
return path.toString();
private void init() throws IOException {
if (!inputSource.isReadable()) throw new FileNotFoundException("File could not be found or is not readable " +
inputSource.getResourceName());
lastModified = inputSource.getLastModified();
}

public long getLength() {
return length;
public String getFilename() {
return inputSource.getResourceName();
}

public long getLastModified() {
return lastModified;
}

}
55 changes: 11 additions & 44 deletions src/main/java/com/mpatric/mp3agic/ID3v1Tag.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Objects;

public class ID3v1Tag implements ID3v1 {

Expand Down Expand Up @@ -227,16 +228,7 @@ public void setComment(String comment) {

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((album == null) ? 0 : album.hashCode());
result = prime * result + ((artist == null) ? 0 : artist.hashCode());
result = prime * result + ((comment == null) ? 0 : comment.hashCode());
result = prime * result + genre;
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((track == null) ? 0 : track.hashCode());
result = prime * result + ((year == null) ? 0 : year.hashCode());
return result;
return Objects.hash(album, artist, comment, genre, title, track, year);
}

@Override
Expand All @@ -247,39 +239,14 @@ public boolean equals(Object obj) {
return false;
if (getClass() != obj.getClass())
return false;
ID3v1Tag other = (ID3v1Tag) obj;
if (album == null) {
if (other.album != null)
return false;
} else if (!album.equals(other.album))
return false;
if (artist == null) {
if (other.artist != null)
return false;
} else if (!artist.equals(other.artist))
return false;
if (comment == null) {
if (other.comment != null)
return false;
} else if (!comment.equals(other.comment))
return false;
if (genre != other.genre)
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
if (track == null) {
if (other.track != null)
return false;
} else if (!track.equals(other.track))
return false;
if (year == null) {
if (other.year != null)
return false;
} else if (!year.equals(other.year))
return false;
return true;
final ID3v1Tag other = (ID3v1Tag) obj;
return
Objects.equals(album, other.album) &&
Objects.equals(artist, other.artist) &&
Objects.equals(comment, other.comment) &&
Objects.equals(genre, other.genre) &&
Objects.equals(title, other.title) &&
Objects.equals(track, other.track) &&
Objects.equals(year, other.year);
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/mpatric/mp3agic/InputSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.mpatric.mp3agic;

import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.OpenOption;

public interface InputSource {

/**
* Retrieves a value that identifies this resource.
* Can be a URL or a file name, for example.
*/
String getResourceName();

/**
* Tells if the resource is valid and readable.
*/
boolean isReadable();

/**
* Retrieves the last modification time of the resource.
*/
long getLastModified() throws IOException;

/**
* Retrieves the length of the resource in bytes.
*/
long getLength() throws IOException;

/**
* Opens a channel to read the resource.
*/
SeekableByteChannel openChannel(OpenOption... options) throws IOException;

}
39 changes: 22 additions & 17 deletions src/main/java/com/mpatric/mp3agic/Mp3File.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,18 @@ public Mp3File(Path path, int bufferLength, boolean scanFile) throws IOException
init(bufferLength, scanFile);
}

public Mp3File(InputSource inputSource, int bufferLength, boolean scanFile) throws IOException, UnsupportedTagException, InvalidDataException {
super(inputSource);
init(bufferLength, scanFile);
}

private void init(int bufferLength, boolean scanFile) throws IOException, UnsupportedTagException, InvalidDataException {
if (bufferLength < MINIMUM_BUFFER_LENGTH + 1) throw new IllegalArgumentException("Buffer too small");

this.bufferLength = bufferLength;
this.scanFile = scanFile;

try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path, StandardOpenOption.READ)) {
try (SeekableByteChannel seekableByteChannel = inputSource.openChannel(StandardOpenOption.READ)) {
initId3v1Tag(seekableByteChannel);
scanFile(seekableByteChannel);
if (startOffset < 0) {
Expand Down Expand Up @@ -146,7 +151,7 @@ private void scanFile(SeekableByteChannel seekableByteChannel) throws IOExceptio
}
lastOffset = startOffset;
}
offset = scanBlock(bytes, bytesRead, fileOffset, offset);
offset = scanBlock(bytes, bytesRead, fileOffset, offset, seekableByteChannel.size());
fileOffset += offset;
seekableByteChannel.position(fileOffset);
break;
Expand Down Expand Up @@ -204,12 +209,12 @@ private int scanBlockForStart(byte[] bytes, int bytesRead, int absoluteOffset, i
return offset;
}

private int scanBlock(byte[] bytes, int bytesRead, int absoluteOffset, int offset) throws InvalidDataException {
private int scanBlock(byte[] bytes, int bytesRead, int absoluteOffset, int offset, long size) throws InvalidDataException {
while (offset < bytesRead - MINIMUM_BUFFER_LENGTH) {
MpegFrame frame = new MpegFrame(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
sanityCheckFrame(frame, absoluteOffset + offset);
sanityCheckFrame(frame, absoluteOffset + offset, size);
int newEndOffset = absoluteOffset + offset + frame.getLengthInBytes() - 1;
if (newEndOffset < maxEndOffset()) {
if (newEndOffset < maxEndOffset(size)) {
endOffset = absoluteOffset + offset + frame.getLengthInBytes() - 1;
frameCount++;
addBitrate(frame.getBitrate());
Expand All @@ -221,8 +226,8 @@ private int scanBlock(byte[] bytes, int bytesRead, int absoluteOffset, int offse
return offset;
}

private int maxEndOffset() {
int maxEndOffset = (int) getLength();
private int maxEndOffset(long size) {
int maxEndOffset = (int) size;
if (hasId3v1Tag()) maxEndOffset -= ID3v1Tag.TAG_LENGTH;
return maxEndOffset;
}
Expand All @@ -249,11 +254,11 @@ private boolean isXingFrame(byte[] bytes, int offset) {
return false;
}

private void sanityCheckFrame(MpegFrame frame, int offset) throws InvalidDataException {
private void sanityCheckFrame(MpegFrame frame, int offset, long size) throws InvalidDataException {
if (sampleRate != frame.getSampleRate()) throw new InvalidDataException("Inconsistent frame header");
if (!layer.equals(frame.getLayer())) throw new InvalidDataException("Inconsistent frame header");
if (!version.equals(frame.getVersion())) throw new InvalidDataException("Inconsistent frame header");
if (offset + frame.getLengthInBytes() > getLength())
if (offset + frame.getLengthInBytes() > size)
throw new InvalidDataException("Frame would extend beyond end of file");
}

Expand All @@ -270,7 +275,7 @@ private void addBitrate(int bitrate) {

private void initId3v1Tag(SeekableByteChannel seekableByteChannel) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(ID3v1Tag.TAG_LENGTH);
seekableByteChannel.position(getLength() - ID3v1Tag.TAG_LENGTH);
seekableByteChannel.position(seekableByteChannel.size() - ID3v1Tag.TAG_LENGTH);
byteBuffer.clear();
int bytesRead = seekableByteChannel.read(byteBuffer);
if (bytesRead < ID3v1Tag.TAG_LENGTH) throw new IOException("Not enough bytes read");
Expand Down Expand Up @@ -302,7 +307,7 @@ private void initId3v2Tag(SeekableByteChannel seekableByteChannel) throws IOExce
}

private void initCustomTag(SeekableByteChannel seekableByteChannel) throws IOException {
int bufferLength = (int) (getLength() - (endOffset + 1));
int bufferLength = (int) (seekableByteChannel.size() - (endOffset + 1));
if (hasId3v1Tag()) bufferLength -= ID3v1Tag.TAG_LENGTH;
if (bufferLength <= 0) {
customTag = null;
Expand Down Expand Up @@ -442,10 +447,13 @@ public void removeCustomTag() {
}

public void save(String newFilename) throws IOException, NotSupportedException {
if (path.toAbsolutePath().compareTo(Paths.get(newFilename).toAbsolutePath()) == 0) {
final Path originalPath = Paths.get(inputSource.getResourceName()).toAbsolutePath();
final Path newPath = Paths.get(newFilename).toAbsolutePath();
if (originalPath.compareTo(newPath) == 0) {
throw new IllegalArgumentException("Save filename same as source filename");
}
try (SeekableByteChannel saveFile = Files.newByteChannel(Paths.get(newFilename), EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE))) {
try (SeekableByteChannel saveFile = Files.newByteChannel(newPath,
EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE))) {
if (hasId3v2Tag()) {
ByteBuffer byteBuffer = ByteBuffer.wrap(id3v2Tag.toBytes());
byteBuffer.rewind();
Expand All @@ -471,9 +479,8 @@ private void saveMpegFrames(SeekableByteChannel saveFile) throws IOException {
if (filePos < 0) filePos = startOffset;
if (filePos < 0) return;
if (endOffset < filePos) return;
SeekableByteChannel seekableByteChannel = Files.newByteChannel(path, StandardOpenOption.READ);
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferLength);
try {
try (SeekableByteChannel seekableByteChannel = inputSource.openChannel(StandardOpenOption.READ)) {
seekableByteChannel.position(filePos);
while (true) {
byteBuffer.clear();
Expand All @@ -489,8 +496,6 @@ private void saveMpegFrames(SeekableByteChannel saveFile) throws IOException {
break;
}
}
} finally {
seekableByteChannel.close();
}
}
}
13 changes: 5 additions & 8 deletions src/main/java/com/mpatric/mp3agic/MutableInteger.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.mpatric.mp3agic;

import java.util.Objects;

public class MutableInteger {

private int value;
Expand All @@ -22,10 +24,7 @@ public void setValue(int value) {

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + value;
return result;
return Objects.hash(value);
}

@Override
Expand All @@ -36,9 +35,7 @@ public boolean equals(Object obj) {
return false;
if (getClass() != obj.getClass())
return false;
MutableInteger other = (MutableInteger) obj;
if (value != other.value)
return false;
return true;
final MutableInteger other = (MutableInteger) obj;
return Objects.equals(value, other.value);
}
}
Loading