/*
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-2005 Christian Pesch. All Rights Reserved.
*/
package slash.metamusic.mp3.tools;
import slash.metamusic.coverdb.LastFmCoverClient;
import slash.metamusic.coverdb.WindowsMediaPlayerCoverClient;
import slash.metamusic.mp3.ID3Genre;
import slash.metamusic.mp3.ID3v2Frame;
import slash.metamusic.mp3.ID3v2Header;
import slash.metamusic.mp3.MP3File;
import slash.metamusic.util.StringHelper;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.logging.Logger;
/**
* A class to
* <ul>
* <li>remove RIFF etc. prefixes from MP3 files</li>
* <li>remove iTunes tags</li>
* <li>remove MusicBrainz tags</li>
* <li>remove MusicMatch tags</li>
* <li>remove Windows Media Player tags</li>
* <li>remove Windows Media Player cover files</li>
* </ul>
*
* @author Christian Pesch
* @version $Id: MP3Cleaner.java 944 2007-01-10 17:12:53Z cpesch $
*/
public class MP3Cleaner extends BaseMP3Modifier {
/**
* Logging output
*/
protected static final Logger log = Logger.getLogger(MP3Cleaner.class.getName());
private static final String MUSICBRAINZ_TRM_ID_PREFIX = "\u0000musicbrainz trm id\u0000";
/**
* Length of a MusicBrain Artist/Album/Track Id
*/
private static final int MB_ID_LENGTH = 36;
private static final int MUSICBRAINZ_TAG_ID_REQUIRED_LENGTH =
MUSICBRAINZ_TRM_ID_PREFIX.length() + MB_ID_LENGTH;
private static final String WMP_PROVIDER_PREFIX = "WM/Provider";
private static final Set<String> ITUNES_TAGS_TO_REMOVE = new TreeSet<String>();
static {
ITUNES_TAGS_TO_REMOVE.addAll(Arrays.asList("TCMP", "TSO2", "TSOC"));
}
private static final Set<String> TAGS_TO_REMOVE = new TreeSet<String>();
static {
TAGS_TO_REMOVE.addAll(Arrays.asList("GEOB", "MCDI", "NCON", "PRIV", "TCOP", "TFLT",
"TMED", "TOPE", "TORY", "TSSE", "TXXX", "XDOR", "XSOP", "WCOM", "WOAF", "WOAR", "WXXX"));
}
private boolean removeiTunesTags = false, removeMusicBrainzTags = true, removeMusicMatchTags = true,
removeWindowsMediaPlayerTags = true, unifyTags = true;
public boolean isRemoveiTunesTags() {
return removeiTunesTags;
}
public void setRemoveiTunesTags(boolean removeiTunesTags) {
this.removeiTunesTags = removeiTunesTags;
}
public boolean isRemoveMusicBrainzTags() {
return removeMusicBrainzTags;
}
public void setRemoveMusicBrainzTags(boolean removeMusicBrainzTags) {
this.removeMusicBrainzTags = removeMusicBrainzTags;
}
public boolean isRemoveMusicMatchTags() {
return removeMusicMatchTags;
}
public void setRemoveMusicMatchTags(boolean removeMusicMatchTags) {
this.removeMusicMatchTags = removeMusicMatchTags;
}
public boolean isRemoveWindowsMediaPlayerTags() {
return removeWindowsMediaPlayerTags;
}
public void setRemoveWindowsMediaPlayerTags(boolean removeWindowsMediaPlayerTags) {
this.removeWindowsMediaPlayerTags = removeWindowsMediaPlayerTags;
}
public boolean isUnifyTags() {
return unifyTags;
}
public void setUnifyTags(boolean unifyTags) {
this.unifyTags = unifyTags;
}
/**
* Clean the given file, i.e.
* <ul>
* <li>remove RIFF etc. prefixes from the file and</li>
* <li>convert its file name to filesystem conventions.</li>
* </ul>
*
* @param file the {@link File} to operate on
* @throws IOException if something wents wrong
*/
public void clean(File file) throws IOException {
MP3File mp3 = MP3File.readValidFile(file);
if (mp3 == null) {
throw new IOException("Invalid MP3 file " + file.getAbsolutePath());
}
clean(mp3);
}
/**
* Clean the given MP3 file, i.e.
* <ul>
* <li>remove RIFF etc. prefixes from the file and</li>
* <li>convert its file name to filesystem conventions.</li>
* </ul>
*
* @param file the {@link MP3File} to operate on
* @throws IOException if something wents wrong
*/
public void clean(MP3File file) throws IOException {
boolean haveToWrite = cleanTags(file);
if (haveToWrite) {
write(file);
log.info("Cleaned " + file.getFile().getAbsolutePath());
}
}
public boolean cleanTags(MP3File file) throws IOException {
boolean removedPrefix = removePrefix(file);
boolean removediTunesTags = isRemoveiTunesTags() && removeiTunesTags(file);
boolean removedMusicBrainzTags = isRemoveMusicBrainzTags() && removeMusicBrainzTags(file);
boolean removedMusicMatchTags = isRemoveMusicMatchTags() && removeMusicMatchTags(file);
boolean removedWindowsMediaPlayerTags = isRemoveWindowsMediaPlayerTags() && removeWindowsMediaPlayerTags(file);
boolean unifiedTagContent = isUnifyTags() && unifyTagContent(file);
boolean removedRedundantTags = isUnifyTags() && removeRedundantTags(file);
boolean cleaned = removedPrefix || removediTunesTags || removedMusicBrainzTags || removedMusicMatchTags ||
removedWindowsMediaPlayerTags || unifiedTagContent || removedRedundantTags;
log.fine("cleaned: " + cleaned + " prefix: " + removedPrefix +
" iTunes: " + removediTunesTags + " MusicBrainz: " + removedMusicBrainzTags +
" MusicMatch: " + removedMusicMatchTags + " WindowsMediaPlayer: " + removedWindowsMediaPlayerTags +
" unified tags: " + unifiedTagContent + " removed tags: " + removedRedundantTags);
return cleaned;
}
public boolean removePrefix(MP3File file) throws IOException {
long prefixSize = file.getProperties().getReadSize() - file.getHead().getReadSize();
if (prefixSize > 0) {
log.info("Found prefix of " + prefixSize + " bytes, removing it");
return true;
}
return false;
}
public boolean removeMusicBrainzTags(MP3File file) {
List<ID3v2Frame> removeHeaders = new ArrayList<ID3v2Frame>();
String trmId = null, releaseDate = null, sortName = null;
ID3v2Header head = file.getHead();
for (ID3v2Frame f : head.getFrames()) {
String content = f.getStringContent();
// TXXX [User defined text information frame]: MusicBrainz TRM Id 01d6885d-1b52-47f0-9b67-55881d66f0da<56 bytes>
// TXXX [User defined text information frame]: MusicBrainz Artist Id b2dbfc09-b332-408b-a235-1850e41971c5<59 bytes>
// TXXX [User defined text information frame]: MusicBrainz Album Id ef663a55-2c1a-4c71-8324-5bca1f88a644<58 bytes>
// TXXX [User defined text information frame]: MusicBrainz Album Type album<29 bytes>
// TXXX [User defined text information frame]: MusicBrainz Album Status official<34 bytes>
// TXXX [User defined text information frame]: MusicBrainz Album Artist Id <29 bytes>
if (f.getTagName().equals("TXXX") && content.toLowerCase().startsWith("\u0000musicbrainz")) {
if (content.toLowerCase().startsWith(MUSICBRAINZ_TRM_ID_PREFIX) &&
content.length() >= MUSICBRAINZ_TAG_ID_REQUIRED_LENGTH) {
trmId = content.substring(MUSICBRAINZ_TRM_ID_PREFIX.length(), MUSICBRAINZ_TAG_ID_REQUIRED_LENGTH);
}
removeHeaders.add(f);
} else if (f.getTagName().equals("XDOR")) {
releaseDate = content;
removeHeaders.add(f);
} else if (f.getTagName().equals("XSOP")) {
sortName = content;
removeHeaders.add(f);
}
}
for (ID3v2Frame f : removeHeaders) {
head.remove(f);
}
if (trmId != null && head.getMusicBrainzId() == null) {
log.info("Transferring MusicBrainzId " + trmId);
head.setMusicBrainzId(trmId);
}
if (releaseDate != null && releaseDate.length() > 0) {
if (head.getFrame("TDRL") == null) {
log.info("Transferring MusicBrainz release date " + releaseDate);
ID3v2Frame f = head.addID3v2Frame("TDRL");
f.setText(releaseDate);
}
if (file.getYear() == -1) {
int releaseYear = parseYear(releaseDate);
if (releaseYear != .1) {
log.info("Transferring MusicBrainz release year " + releaseYear);
file.setYear(releaseYear);
}
}
}
if (sortName != null && sortName.length() > 0) {
if (head.getFrame("TSOP") == null) {
log.info("Transferring MusicBrainz sort name " + sortName);
ID3v2Frame f = head.addID3v2Frame("TSOP");
f.setText(sortName);
}
}
boolean result = removeHeaders.size() > 0;
if (result)
log.info("Removed " + removeHeaders.size() + " MusicBrainz headers");
return result;
}
public boolean removeMusicMatchTags(MP3File file) {
List<ID3v2Frame> removeHeaders = new ArrayList<ID3v2Frame>();
ID3v2Header head = file.getHead();
for (ID3v2Frame f : head.getFrames()) {
String content = f.getStringContent();
if (f.getTagName().equals("COMM")) {
// COMM [Comments]: English(eng),MusicMatch_Tempo,Moderate
// COMM [Comments]: English(eng),MusicMatch_Preference,None
if (content.contains("MusicMatch_Tempo") || content.contains("MusicMatch_Preference")) {
removeHeaders.add(f);
}
}
}
for (ID3v2Frame f : removeHeaders) {
head.remove(f);
}
boolean result = removeHeaders.size() > 0;
if (result)
log.info("Removed " + removeHeaders.size() + " MusicMatch headers");
return result;
}
public boolean removeWindowsMediaPlayerTags(MP3File file) {
List<ID3v2Frame> removeHeaders = new ArrayList<ID3v2Frame>();
String publisher = null;
ID3v2Header head = file.getHead();
for (ID3v2Frame f : head.getFrames()) {
String content = f.getStringContent();
// PRIV [Private frame]: WM/MediaClassPrimaryID...<39 bytes>
// PRIV [Private frame]: WM/MediaClassSecondaryID...<41 bytes>
// PRIV [Private frame]: WM/Provider...<20 bytes>
// PRIV [Private frame]: WM/UniqueFileIdentifier...<138 bytes>
// PRIV [Private frame]: WM/WMCollectionID...<34 bytes>
// PRIV [Private frame]: WM/WMCollectionGroupID...<39 bytes>
// PRIV [Private frame]: WM/WMContentID...<31 bytes>
// PRIV [Private frame]: PeakValue ...<14 bytes>
// PRIV [Private frame]: AverageLevel ...<17 bytes>
if (f.getTagName().equals("PRIV")) {
if (content.startsWith(WMP_PROVIDER_PREFIX)) {
String temp = content.substring(WMP_PROVIDER_PREFIX.length());
int strip = temp.indexOf('<') - 1;
if (strip > 0 && strip < temp.length())
temp = temp.substring(0, strip);
try {
publisher = new String(temp.getBytes(), "UTF16");
publisher = StringHelper.trim(publisher);
} catch (UnsupportedEncodingException e) {
// cannot happen as UTF16 is builtin
}
}
// move tags to regular ID3v2 tags as shown on http://www.adhitsoft.com/jsp/product_pgvtag_tagmap.jsp?
if (content.startsWith("WM/") || content.startsWith("AverageLevel") ||
content.startsWith("PeakValue")) {
removeHeaders.add(f);
}
}
}
for (ID3v2Frame f : removeHeaders) {
head.remove(f);
}
if (publisher != null && file.getHead().getPublisher() == null) {
log.info("Transferring publisher " + publisher);
file.getHead().setPublisher(publisher);
}
boolean result = removeHeaders.size() > 0;
if (result)
log.info("Removed " + removeHeaders.size() + " Windows Media Player headers");
return result;
}
public boolean removeiTunesTags(MP3File file) {
List<ID3v2Frame> removeHeaders = new ArrayList<ID3v2Frame>();
ID3v2Header head = file.getHead();
for (ID3v2Frame f : head.getFrames()) {
String name = f.getTagName();
if (name.equals("COMM")) {
String description = f.getDescription();
if (description.startsWith("iTun")) {
removeHeaders.add(f);
}
}
if (ITUNES_TAGS_TO_REMOVE.contains(name)) {
removeHeaders.add(f);
}
}
for (ID3v2Frame f : removeHeaders) {
head.remove(f);
}
boolean result = removeHeaders.size() > 0;
if (result)
log.info("Removed " + removeHeaders.size() + " iTunes headers");
return result;
}
public boolean removeRedundantTags(MP3File file) {
List<ID3v2Frame> removeHeaders = new ArrayList<ID3v2Frame>();
ID3v2Header head = file.getHead();
for (ID3v2Frame f : head.getFrames()) {
String content = f.getStringContent();
if (f.getTagName().equals("TBPM") && "0".equals(content)) {
removeHeaders.add(f);
}
if (f.getTagName().equals("TOPE") && "".equals(content)) {
removeHeaders.add(f);
}
if (f.getTagName().equals("TCON") && ("(0)".equals(content) || content.startsWith(ID3Genre.UNKNOWN))) {
removeHeaders.add(f);
}
if (f.getTagName().equals("COMM")) {
// TODO this is for some very broken files
if (!("iTunNORM".equals(f.getDescription()) || "Written".equals(f.getDescription()))) {
removeHeaders.add(f);
}
}
if (TAGS_TO_REMOVE.contains(f.getTagName())) {
removeHeaders.add(f);
}
}
String artist = file.getArtist();
String albumArtist = file.getHead().getAlbumArtist();
if (artist != null && albumArtist != null && artist.equals(albumArtist)) {
log.info("Removing album artist " + albumArtist + " since its the same as artist");
removeHeaders.add(new ID3v2Frame("TPE2"));
}
String track = file.getTrack();
String group = file.getHead().getStringContent("TIT1");
if (track != null && group != null && track.equals(group)) {
log.info("Removing content group description " + group + " since its the same as track");
removeHeaders.add(new ID3v2Frame("TIT1"));
}
if (file.getPartOfSetCount() == 1 && file.getPartOfSetIndex() == 1) {
removeHeaders.add(new ID3v2Frame("TPOS"));
}
for (ID3v2Frame f : removeHeaders) {
head.remove(f);
}
boolean result = removeHeaders.size() > 0;
if (result)
log.info("Removed " + removeHeaders.size() + " irrelevant headers");
return result;
}
private int parseYear(String year) {
try {
if (year != null) {
if (year.length() > 4)
year = year.substring(0, 4);
return Integer.parseInt(year);
}
}
catch (NumberFormatException e) {
// don't care
}
return -1;
}
public boolean unifyTagContent(MP3File file) {
int count = 0;
int releaseYear = -1;
String encoder = null;
ID3v2Header head = file.getHead();
for (ID3v2Frame f : head.getFrames()) {
String content = f.getStringContent();
if (f.getTagName().equals("TLAN") && content.toLowerCase().equals("eng")) {
f.setText("English");
count++;
}
if (f.getTagName().equals("TORY")) {
releaseYear = parseYear(content);
count++;
}
if (f.getTagName().equals("TSSE")) {
encoder = content;
count++;
}
}
if (releaseYear != -1 && file.getYear() == -1) {
log.info("Transferring release year " + releaseYear);
file.setYear(releaseYear);
}
if (encoder != null && head.getFrame("TENC") == null) {
log.info("Transferring encoder " + encoder);
ID3v2Frame f = head.addID3v2Frame("TENC");
f.setText(encoder);
}
boolean result = count > 0;
if (result)
log.info("Unified " + count + " headers");
return result;
}
public void removeCovers(File file) {
removeWindowsMediaPlayerCovers(file);
removeLastFmCovers(file);
}
private void removeWindowsMediaPlayerCovers(File file) {
WindowsMediaPlayerCoverClient wmpClient = new WindowsMediaPlayerCoverClient();
wmpClient.removeCover(file);
}
private void removeLastFmCovers(File file) {
LastFmCoverClient lfClient = new LastFmCoverClient();
lfClient.removeCover(file);
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.out.println("slash.metamusic.mp3.tools.MP3Cleaner <file>");
System.exit(1);
}
File file = new File(args[0]);
MP3Cleaner cleaner = new MP3Cleaner();
cleaner.clean(file);
cleaner.removeCovers(file);
System.exit(0);
}
}