/*
You may freely copy, distribute, modify and use this class as long
as the original author attribution remains intact. See message
below.
Copyright (C) 2003 Christian Pesch. All Rights Reserved.
*/
package slash.metamusic.itunes.com;
import slash.metamusic.itunes.com.binding.IITFileOrCDTrack;
import slash.metamusic.itunes.com.binding.IITTrackCollection;
import slash.metamusic.mp3.MP3File;
import slash.metamusic.mp3.tools.BaseMP3Modifier;
import java.io.File;
import java.text.DateFormat;
import java.util.*;
import java.util.logging.Logger;
import static java.lang.Math.abs;
/**
* Syncs the iTunes library rating, play count and play time with the tags
* of the MP3 files contained in the library.
*
* @author Christian Pesch
* @version $Id: iTunesCOMSynchronizer.java 792 2006-04-22 10:09:35 +0200 (Sa, 22 Apr 2006) cpesch $
*/
public class iTunesCOMSynchronizer extends BaseMP3Modifier {
protected static final Logger log = Logger.getLogger(iTunesCOMSynchronizer.class.getName());
private static DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance();
static {
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private static final int MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
private static final long MP3_SIZE_LIMIT_BYTES = 16 * 1024 * 1024;
private iTunesCOMLibrary library = new iTunesCOMLibrary();
private List<Notifier> notifiers = new ArrayList<Notifier>();
private boolean addPlayCount = false;
private IITTrackCollection tracks;
private int trackCount, processedTrackCount, failedTrackCount, modifiedFileCount, modifiedTrackCount, removedTrackCount;
public iTunesCOMSynchronizer() {
addNotifier(new LogNotifier());
}
public boolean isAddPlayCount() {
return addPlayCount;
}
public void setAddPlayCount(boolean addPlayCount) {
this.addPlayCount = addPlayCount;
}
public boolean addNotifier(Notifier notifier) {
return notifiers.add(notifier);
}
public boolean isiTunesSupported() {
return iTunesCOMLibrary.isSupported();
}
public void open() {
library.open();
for (Notifier notifier : notifiers)
notifier.opened(library.getVersion(), library.getLibraryPath(), library.getTrackCount(), library.getPlaylistCount());
}
public void close() {
library.close();
}
public void start() {
for (Notifier notifier : notifiers)
notifier.started(library.getTrackCount(), library.getPlaylistCount());
processedTrackCount = 1;
modifiedFileCount = 0;
modifiedTrackCount = 0;
removedTrackCount = 0;
tracks = library.getTracks();
trackCount = tracks.getCount();
}
public boolean next() {
if (processedTrackCount > trackCount) {
for (Notifier notifier : notifiers)
notifier.finished(modifiedFileCount, modifiedTrackCount, removedTrackCount);
return false;
} else {
for (Notifier notifier : notifiers)
notifier.processing(processedTrackCount);
String location = null;
try {
location = getNextLocation();
}
catch (Throwable t) {
log.severe("Track " + processedTrackCount + " has no location: " + t.getMessage());
// avoids endless loops when the track has no location
processedTrackCount++;
}
if(location != null) {
try {
processNext(true, location);
} catch (Throwable t) {
t.printStackTrace();
log.severe("Error while processing track " + processedTrackCount + " at " + location + ": " + t.getMessage());
failedTrackCount++;
for (Notifier notifier : notifiers)
notifier.failed(failedTrackCount, location);
}
}
return true;
}
}
private String getNextLocation() {
IITFileOrCDTrack track = tracks.getFileOrCDTrack(processedTrackCount);
return track.getLocation();
}
private void processNext(boolean firstTime, String location) {
try {
IITFileOrCDTrack track = tracks.getFileOrCDTrack(processedTrackCount);
if (!canProcess(track)) {
remove(track);
} else {
try {
process(track);
}
finally {
// increase count for IITFileOrCDTrack access only if file existed
// TODO split processedTrackCount and getFileOrCDTrackCount
processedTrackCount++;
}
}
}
catch (Exception e) {
e.printStackTrace();
log.severe("Error while processing track " + processedTrackCount + " at " + location + ": " + e.getMessage());
// wait and have one more chance
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
// intentionally left empty
}
// one more chance
if (firstTime)
processNext(false, location);
else
// avoids endless loops I've seen accessing the same track again and again
processedTrackCount++;
}
}
protected boolean canProcess(IITFileOrCDTrack track) {
String location = track.getLocation();
if (location == null || location.length() == 0) {
log.warning("Cannot process location-less track " + track);
return false;
}
File file = new File(location);
if (!file.exists()) {
log.warning("Cannot process not-existing file " + location);
return false;
}
return true;
}
protected void remove(IITFileOrCDTrack track) {
String location = track.getLocation();
if (location == null || location.length() == 0)
location = track.getName() + ", " + track.getAlbum() + ", " + track.getArtist() + ", " + track.gettrackID();
track.delete();
removedTrackCount++;
for (Notifier notifier : notifiers)
notifier.removed(removedTrackCount, location);
}
protected void process(IITFileOrCDTrack track) {
File file = new File(track.getLocation());
Date iTunesModificationDate = track.getModificationDate();
long iTunesModificationTime = iTunesModificationDate.getTime() / 1000;
long fileModificationTime = file.lastModified() / 1000;
if (iTunesModificationTime < fileModificationTime) {
log.info("File modified after library modification " + iTunesModificationDate +
" diff: " + (iTunesModificationTime - fileModificationTime) + " seconds");
} else if (iTunesModificationTime == fileModificationTime) {
log.info("File modification matches library modification " + iTunesModificationDate);
} else {
log.info("Library modified after file modification " + iTunesModificationDate +
" diff: " + (fileModificationTime - iTunesModificationTime) + " seconds");
}
if (file.length() > MP3_SIZE_LIMIT_BYTES)
return;
MP3File mp3 = MP3File.readValidFile(file);
if (mp3 != null)
process(track, mp3);
else {
failedTrackCount++;
for (Notifier notifier : notifiers)
notifier.failed(failedTrackCount, file.getAbsolutePath());
}
}
private boolean needToUpdateMP3(Date iTunesPlayTime, Date mp3PlayTime) {
if(iTunesPlayTime == null)
return false;
if(mp3PlayTime == null)
return true;
long diff = abs(iTunesPlayTime.getTime() - mp3PlayTime.getTime());
// avoid updates if the difference is exactly one or two hours
return iTunesPlayTime.after(mp3PlayTime) && (diff != MILLISECONDS_PER_HOUR && diff != 2 * MILLISECONDS_PER_HOUR);
}
protected void process(IITFileOrCDTrack track, MP3File mp3) {
boolean fileModified = false, trackModified = false;
int mp3Rating = mp3.getHead().getRating();
if (mp3Rating > 0 && track.getRating() == 0) {
log.info("Modifying library rating to " + mp3Rating);
track.setRating(mp3Rating);
trackModified = true;
}
int iTunesRating = track.getRating();
if (track.getRating() > 0 && iTunesRating != mp3Rating) {
log.info("Modifying rating from '" + mp3.getFile().getAbsolutePath() + "' to " + iTunesRating + " from " + mp3Rating);
mp3.getHead().setRating(iTunesRating);
fileModified = true;
}
if (isAddPlayCount()) {
int mp3PlayCount = mp3.getHead().getPlayCount();
if (mp3PlayCount < 0)
mp3PlayCount = 0;
int iTunesPlayCount = track.getPlayedCount();
int playCount = mp3PlayCount + iTunesPlayCount;
if (playCount > 0) {
log.info("Adding library play count to " + playCount + " from " + iTunesPlayCount);
track.setPlayedCount(playCount);
trackModified = true;
log.info("Adding play count of '" + mp3.getFile().getAbsolutePath() + "' to " + playCount + " from " + mp3PlayCount);
mp3.getHead().setPlayCount(playCount);
fileModified = true;
}
} else {
int mp3PlayCount = mp3.getHead().getPlayCount();
int iTunesPlayCount = track.getPlayedCount();
if (mp3PlayCount > 0 && mp3PlayCount > iTunesPlayCount) {
log.info("Modifying library play count to " + mp3PlayCount + " from " + iTunesPlayCount);
track.setPlayedCount(mp3PlayCount);
trackModified = true;
}
if (iTunesPlayCount > 0 && iTunesPlayCount > mp3PlayCount) {
log.info("Modifying play count of '" + mp3.getFile().getAbsolutePath() + "' to " + iTunesPlayCount + " from " + mp3PlayCount);
mp3.getHead().setPlayCount(iTunesPlayCount);
fileModified = true;
}
}
Date mp3PlayTime = mp3.getHead().getPlayTime() != null ? mp3.getHead().getPlayTime().getTime() : null;
if (mp3PlayTime != null && (track.getPlayedDate() == null || mp3PlayTime.after(track.getPlayedDate()))) {
log.info("Modifying library play time to " + DATE_FORMAT.format(mp3PlayTime.getTime()));
track.setPlayedDate(mp3PlayTime);
trackModified = true;
}
Date iTunesPlayTime = track.getPlayedDate();
if (needToUpdateMP3(iTunesPlayTime, mp3PlayTime)) {
log.info("Modifying play time from '" + mp3.getFile().getAbsolutePath() + "' to " + DATE_FORMAT.format(iTunesPlayTime.getTime()) +
" from " + (mp3PlayTime != null ? DATE_FORMAT.format(mp3PlayTime.getTime()) : "<null>"));
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(iTunesPlayTime);
mp3.getHead().setPlayTime(calendar);
fileModified = true;
}
if (fileModified) {
modifiedFileCount++;
write(mp3);
}
if (trackModified)
modifiedTrackCount++;
for (Notifier notifier : notifiers)
notifier.processed(modifiedFileCount, modifiedTrackCount, mp3.getFile().getAbsolutePath(), fileModified, trackModified);
}
public interface Notifier {
void opened(String version, String libraryPath, int trackCount, int playlistCount);
void started(int trackCount, int playlistCount);
void processing(int processedTracks);
void failed(int failedTrackCount, String location);
void removed(int removedTrackCount, String location);
void processed(int modifiedFileCount, int modifiedTrackCount, String location, boolean fileModified, boolean trackModified);
void finished(int modifiedFileCount, int modifiedTrackCount, int removedTrackCount);
}
private static class LogNotifier implements Notifier {
private int trackCount = 0, processedTracks = 0;
public void opened(String version, String libraryPath, int trackCount, int playlistCount) {
log.info("Connected to iTunes " + version + " with library from '" + libraryPath + "'");
}
public void started(int trackCount, int playlistCount) {
this.trackCount = trackCount;
log.info("Library contains " + trackCount + " tracks and " + playlistCount + " playlists");
}
public void processing(int processedTracks) {
this.processedTracks = processedTracks;
log.info("Processing " + processedTracks + ". from " + trackCount + " tracks");
}
public void failed(int failedTrackCount, String location) {
log.info("Failed to process " + failedTrackCount + ". track: " + location);
}
public void removed(int removedTrackCount, String location) {
log.info("Removed " + removedTrackCount + ". track for not-existing file: " + location);
}
public void processed(int modifiedFileCount, int modifiedTrackCount, String location, boolean fileModified, boolean trackModified) {
if (fileModified)
log.info("Modified " + modifiedFileCount + ". file: " + location);
if (trackModified)
log.info("Modified " + modifiedTrackCount + ". track: " + location);
}
public void finished(int modifiedFileCount, int modifiedTrackCount, int removedTrackCount) {
log.info("Processed " + processedTracks + " out of " + trackCount + " tracks");
log.info("Modified " + modifiedFileCount + " out of " + processedTracks + " processed files");
log.info("Modified " + modifiedTrackCount + " out of " + trackCount + " tracks");
log.info("Removed " + removedTrackCount + " out of " + trackCount + " tracks");
}
}
}