/* 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.xml; import slash.metamusic.itunes.xml.binding.Array; import slash.metamusic.itunes.xml.binding.Dict; import slash.metamusic.itunes.xml.binding.Plist; import slash.metamusic.itunes.xml.iTunesXMLLibrary.Playlist; import slash.metamusic.itunes.xml.iTunesXMLLibrary.Track; import slash.metamusic.mp3.MP3File; import slash.metamusic.util.Files; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.transform.stream.StreamSource; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.net.URL; import java.text.DateFormat; import java.util.*; import java.util.logging.Logger; /** * 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: iTunesXMLSynchronizer.java 792 2006-04-22 10:09:35 +0200 (Sa, 22 Apr 2006) cpesch $ */ public class iTunesXMLSynchronizer { /** * Logging output */ protected static final Logger log = Logger.getLogger(iTunesXMLSynchronizer.class.getName()); private static DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance(); static { DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); } private File library; private boolean addPlayCount; private int fileCount, libraryEntryCount, modifiedFileCount, modifiedLibraryEntryCount, removedLibraryEntryCount; public File getLibrary() { return library; } public void setLibrary(File library) { this.library = library; } public boolean isAddPlayCount() { return addPlayCount; } public void setAddPlayCount(boolean addPlayCount) { this.addPlayCount = addPlayCount; } private List<Track> mapToTracks(Dict dict) { List<Track> result = new ArrayList<Track>(); List<Object> objects = dict.getKeyAndArrayOrData(); for (int i = 0; i < objects.size(); i += 2) { JAXBElement key = (JAXBElement) objects.get(i); Dict value = (Dict) objects.get(i + 1); Track track = new Track(dict, key, value); result.add(track); } return result; } private List<Playlist> mapToPlaylists(Array array) { List<Playlist> result = new ArrayList<Playlist>(); List<Object> objects = array.getArrayOrDataOrDate(); for (Object object : objects) { Dict value = (Dict) object; Playlist playlist = new Playlist(value); result.add(playlist); } return result; } public void start() { try { log.info("Parsing iTunes library from '" + getLibrary().getAbsolutePath() + "'"); Plist plist = (Plist) iTunesXMLLibrary.unmarshal(Plist.class, new StreamSource(new FileReader(getLibrary()))); log.fine("Plist version: " + plist.getVersion()); Dict propertyDict = plist.getDict(); Map<String, Object> properties = iTunesXMLLibrary.dictToMap(propertyDict); log.fine("Properties (" + properties.size() + "): " + properties); List<Track> tracks = mapToTracks((Dict) properties.get("Tracks")); libraryEntryCount = tracks.size(); log.fine("Tracks (" + libraryEntryCount + "): " + tracks); List<Playlist> playlists = mapToPlaylists((Array) properties.get("Playlists")); log.fine("Playlists (" + playlists.size() + "): " + playlists); process(tracks); // since playlists are removed anyway... // Set<Track> accessibleTracks = process(tracks); // process(playlists, accessibleTracks); removePlaylists(plist); File modifiedLibrary = new File(Files.replaceExtension(getLibrary().getAbsolutePath(), "import.xml")); log.info("Writing iTunes library to '" + modifiedLibrary.getAbsolutePath() + "'"); iTunesXMLLibrary.marshal("", "plist", Plist.class, plist, new FileOutputStream(modifiedLibrary)); } catch (JAXBException e) { log.severe("Cannot parse iTunes library '" + getLibrary() + "': " + e.getMessage()); } catch (FileNotFoundException e) { log.severe("iTunes library '" + getLibrary() + "' not found: " + e.getMessage()); } finally { log.info("Modified " + modifiedFileCount + " out of " + fileCount + " files"); log.info("Modified " + modifiedLibraryEntryCount + " iTunes library entries out of " + libraryEntryCount); log.info("Removed " + removedLibraryEntryCount + " iTunes library entries out of " + libraryEntryCount); } } protected void process(List<Playlist> playlists, Set<Track> accessibleTracks) { for (Playlist playlist : playlists) { remove(playlist, accessibleTracks); } } protected Set<Track> process(List<Track> tracks) { Set<Track> accessibleTracks = new HashSet<Track>(); for (Track track : tracks) { if (!canProcess(track)) { removedLibraryEntryCount++; remove(track); } else { log.info("Processed " + (++fileCount) + ". from " + libraryEntryCount + " tracks with id: " + track.getId()); accessibleTracks.add(track); process(track); } } return accessibleTracks; } protected boolean canProcess(Track track) { URL location = track.getLocation(); if (location == null) { log.warning("Cannot process location-less track " + track); return false; } if (!"file".equals(location.getProtocol())) { log.warning("Cannot process non-file track " + location); return false; } File file = new File(location.getFile()); if (!file.exists()) { log.warning("Cannot process not-existing file " + file); return false; } return true; } protected boolean remove(Track track) { boolean result = track.delete(); if (result) { log.info("Removed track " + track.getId() + " for not-existing file " + track.getLocation()); } return result; } protected boolean remove(Playlist playlist, Set<Track> accessibleTracks) { int result = playlist.delete(accessibleTracks); if (result > 0) log.info("Removed " + result + " unaccessible tracks from play list " + playlist.getName()); return result > 0; } protected void process(Track track) { URL location = track.getLocation(); File file = new File(location.getFile()); // TODO avoid parsing the MP3 by comparing file and library modification timestamps if (track.getSynchronizationTime() != null) { if (track.getSynchronizationTime().getTimeInMillis() < file.lastModified()) { log.info("File modified after library synchronization " + track.getSynchronizationTime() + " diff:" + (track.getSynchronizationTime().getTimeInMillis() - file.lastModified())); } if (track.getModificationTime() != null && track.getSynchronizationTime().getTimeInMillis() < track.getModificationTime().getTimeInMillis()) { log.info("Library synchronization after file modified " + track.getSynchronizationTime() + " diff:" + (track.getSynchronizationTime().getTimeInMillis() - track.getModificationTime().getTimeInMillis())); } } MP3File mp3 = MP3File.readValidFile(file); if (mp3 != null) process(track, mp3); else log.warning("Could not read " + file.getAbsolutePath()); } protected void process(Track track, MP3File mp3) { boolean fileModified = false, libraryEntryModified = false; int mp3Rating = mp3.getHead().getRating(); if (mp3Rating > 0 && track.getRating() == null) { log.info("Modifying library rating to " + mp3Rating); track.setRating(mp3Rating); libraryEntryModified = true; } int iTunesRating = track.getRating() != null ? track.getRating() : 0; if (iTunesRating > 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.getPlayCount() != null ? track.getPlayCount() : 0; int playCount = mp3PlayCount + iTunesPlayCount; if (playCount > 0) { log.info("Adding library play count to " + playCount + " from " + iTunesPlayCount); track.setPlayCount(playCount); libraryEntryModified = 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(); if (mp3PlayCount > 0 && (track.getPlayCount() == null || mp3PlayCount > track.getPlayCount())) { int iTunesPlayCount = track.getPlayCount() != null ? track.getPlayCount() : 0; log.info("Modifying library play count to " + mp3PlayCount + " from " + iTunesPlayCount); track.setPlayCount(mp3PlayCount); libraryEntryModified = true; } int iTunesPlayCount = track.getPlayCount() != null ? track.getPlayCount() : -1; if (iTunesPlayCount > mp3PlayCount) { log.info("Modifying play count of '" + mp3.getFile().getAbsolutePath() + "' to " + iTunesPlayCount + " from " + mp3PlayCount); mp3.getHead().setPlayCount(iTunesPlayCount); fileModified = true; } } Calendar mp3PlayTime = mp3.getHead().getPlayTime(); if (mp3PlayTime != null && (track.getPlayTime() == null || mp3PlayTime.after(track.getPlayTime()))) { log.info("Modifying library play time to " + DATE_FORMAT.format(mp3PlayTime.getTime())); track.setPlayTime(mp3PlayTime); libraryEntryModified = true; } Calendar iTunesPlayTime = track.getPlayTime(); if (iTunesPlayTime != null && (mp3PlayTime == null || iTunesPlayTime.after(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>")); mp3.getHead().setPlayTime(iTunesPlayTime); fileModified = true; } track.setSynchronizationTime(Calendar.getInstance()); if (fileModified) { modifiedFileCount++; log.info("Writing '" + mp3.getFile().getAbsolutePath()); // TODO make this dependant on user interaktion or profile later mp3.setID3v2(true); mp3.setID3v1(false); try { mp3.write(); } catch (Exception e) { log.severe("Could not write mp3 " + mp3 + ": " + e.getMessage()); } } if (libraryEntryModified) modifiedLibraryEntryCount++; } protected void removePlaylists(Plist plist) { int removeIndex = -1; List<Object> objects = plist.getDict().getKeyAndArrayOrData(); for (int i = 0; i < objects.size(); i += 2) { JAXBElement key = (JAXBElement) objects.get(i); if ("Playlists".equals(key.getValue())) { removeIndex = i; } } if (removeIndex != -1) { objects.remove(removeIndex + 1); objects.remove(removeIndex); } } }