Skip to content

Instantly share code, notes, and snippets.

@autermann
Created June 19, 2015 01:06
Show Gist options
  • Save autermann/f3991d207d5caa80e8c8 to your computer and use it in GitHub Desktop.
Save autermann/f3991d207d5caa80e8c8 to your computer and use it in GitHub Desktop.
package org.autermann.itunessync;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
/**
* @author Christian Autermann
*/
public class PlaylistSyncer {
public static void main(String[] args) throws Exception {
new PlaylistSyncer().run(args);
}
public void run(String[] args) throws IOException {
run(args[0], Paths.get(args[1]));
}
private void run(String playlistName, Path destination) throws IOException {
LibaryAnalyzer analyzer = new LibaryAnalyzer(getLibraryPath());
Playlist playlist = analyzer.getTracks(playlistName);
Syncer syncer = new Syncer(playlist.getBasePath(), destination);
syncer.sync(playlist.getTracks());
}
private static Path getLibraryPath() {
String home = System.getProperty("user.home");
return Paths.get(home, "iTunes", "iTunes Music Library.xml");
}
private static class Syncer {
private final Path source;
private final Path destination;
public Syncer(Path source, Path destination) {
this.source = source;
this.destination = destination;
}
public void sync(List<Path> paths) throws IOException {
Set<Path> sourcePaths = paths.stream()
.flatMap(this::getParentPaths)
.collect(Collectors.toSet());
if (Files.exists(destination)) {
Files.walkFileTree(destination, new DestinationCleaner(sourcePaths));
}
for (Path path : paths) {
Path dst = destination.resolve(path);
if (!Files.exists(dst)) {
Path src = source.resolve(path);
System.out.printf("Copying %s\n", path);
Files.createDirectories(dst.getParent());
Files.copy(src, dst);
FileTime lmt = Files.getLastModifiedTime(src);
Files.setLastModifiedTime(dst, lmt);
} else {
System.out.printf("Skipping %s\n", path);
}
}
}
private Stream<Path> getParentPaths(Path path) {
return IntStream.rangeClosed(1, path.getNameCount())
.mapToObj(i -> path.subpath(0, i));
}
private static class DeletingFileVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
}
private class DestinationCleaner extends SimpleFileVisitor<Path> {
private final Set<Path> sourcePaths;
public DestinationCleaner(Set<Path> sourcePaths) {
this.sourcePaths = sourcePaths;
}
@Override
public FileVisitResult preVisitDirectory(Path destinationDir, BasicFileAttributes attrs) throws IOException {
if (destinationDir.equals(destination)) {
return FileVisitResult.CONTINUE;
}
Path relativePath = destination.relativize(destinationDir);
Path sourceDir = source.resolve(relativePath);
if (!sourcePaths.contains(relativePath) || !Files.isDirectory(sourceDir)) {
System.out.printf("Deleting %s\n", destinationDir);
Files.walkFileTree(destinationDir, new DeletingFileVisitor());
return FileVisitResult.SKIP_SUBTREE;
} else {
return FileVisitResult.CONTINUE;
}
}
@Override
public FileVisitResult visitFile(Path destinationFile, BasicFileAttributes attrs) throws IOException {
Path relativePath = destination.relativize(destinationFile);
Path sourceFile = source.resolve(relativePath);
if (!sourcePaths.contains(relativePath) ||
Files.isDirectory(sourceFile) ||
Files.size(sourceFile) != Files.size(destinationFile) ||
attrs.lastModifiedTime().compareTo(Files.getLastModifiedTime(sourceFile)) != 0) {
System.out.printf("Deleting %s\n", destinationFile);
Files.delete(destinationFile);
}
return FileVisitResult.CONTINUE;
}
}
}
private static class LibaryAnalyzer {
private static final String NAME = "Name";
private static final String PLAYLISTS = "Playlists";
private static final String TRACK_ID = "Track ID";
private static final String PLAYLIST_ITEMS = "Playlist Items";
private static final String LOCATION = "Location";
private static final String TRACKS = "Tracks";
private static final String MUSIC_FOLDER = "Music Folder";
private final Map<String, Object> library;
public LibaryAnalyzer(Path path) throws IOException {
LibraryReader reader = new LibraryReader();
this.library = asDict(reader.read(path).get(0));
}
public Playlist getTracks(String playListName) throws IOException {
Path musicFolder = getMusicFolder();
List<Path> files = getPaths(playListName)
.map(musicFolder::relativize)
.collect(Collectors.toList());
return new Playlist(musicFolder, files);
}
private Path getMusicFolder() {
return asPath((String) library.get(MUSIC_FOLDER));
}
private Stream<Path> getPaths(String playlistName) {
Map<String, Object> tracks = getTracks();
return getPlaylist(playlistName)
.map(x -> Long.toString((long) x.get(TRACK_ID)))
.map(tracks::get).map(this::asDict)
.map(x -> (String) x.get(LOCATION))
.map(this::asPath);
}
private Map<String, Object> getTracks() {
return asDict(library.get(TRACKS));
}
private Stream<Map<String, Object>> getPlaylist(String playlistName) {
return getPlaylists().parallelStream().map(this::asDict)
.filter(x -> x.get(NAME) != null && x.get(NAME).equals(playlistName))
.findAny().map(x -> x.get(PLAYLIST_ITEMS)).map(this::asArray)
.map(List::parallelStream).orElseGet(Stream::empty).map(this::asDict);
}
private List<Object> getPlaylists() {
return asArray(library.get(PLAYLISTS));
}
private Path asPath(String path) {
return Paths.get(URI.create(path.replace("file://localhost/", "file:///")));
}
private Map<String, Object> asDict(Object o) {
return (Map<String, Object>) o;
}
private List<Object> asArray(Object o) {
return (List<Object>) o;
}
}
private static class Playlist {
private final Path basePath;
private final List<Path> tracks;
public Playlist(Path basePath, List<Path> tracks) {
this.basePath = basePath;
this.tracks = tracks;
}
public Path getBasePath() {
return basePath;
}
public List<Path> getTracks() {
return tracks;
}
}
private static class LibraryReader {
private static final Pattern LINE_BREAK = Pattern.compile("\\r?\\n");
private static final String PREMATURE_EOF = "Premature end of file";
private static final String PLIST = "plist";
private static final String DATE_TYPE = "date";
private static final String DATA_TYPE = "data";
private static final String REAL_TYPE = "real";
private static final String FALSE_TYPE = "false";
private static final String TRUE_TYPE = "true";
private static final String INTEGER_TYPE = "integer";
private static final String STRING_TYPE = "string";
private static final String KEY_TYPE = "key";
private static final String DICT_TYPE = "dict";
private static final String ARRAY_TYPE = "array";
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final XMLInputFactory inputFactory = XMLInputFactory.newFactory();
public List<Object> read(Path file) throws IOException {
return read(Files.newBufferedReader(file, UTF_8));
}
public List<Object> read(Reader in) throws IOException {
XMLStreamReader reader = null;
try (Reader inr = in) {
reader = this.inputFactory.createXMLStreamReader(inr);
return readDocument(reader);
} catch (XMLStreamException ex) {
throw new IOException(ex);
} finally {
if (reader != null) {
try {
reader.close();
} catch (XMLStreamException ex) {
//ignore
}
}
}
}
private List<Object> readDocument(XMLStreamReader reader) throws XMLStreamException {
while (reader.hasNext()) {
int eventType = reader.next();
switch (eventType) {
case XMLStreamReader.START_ELEMENT:
String elementName = reader.getLocalName();
if (elementName.equals(PLIST)) {
return readPList(reader);
}
break;
case XMLStreamReader.END_ELEMENT:
break;
}
}
throw new XMLStreamException(PREMATURE_EOF);
}
private List<Object> readPList(XMLStreamReader reader) throws XMLStreamException {
return readList(reader);
}
private List<Object> readList(XMLStreamReader reader) throws XMLStreamException {
List<Object> objects = new ArrayList<>();
while (reader.hasNext()) {
int eventType = reader.next();
switch (eventType) {
case XMLStreamReader.START_ELEMENT:
objects.add(readValue(reader));
break;
case XMLStreamReader.END_ELEMENT:
return objects;
}
}
throw new XMLStreamException(PREMATURE_EOF);
}
private Object readValue(XMLStreamReader reader) throws XMLStreamException {
final String name = reader.getLocalName();
switch (name) {
case ARRAY_TYPE:
return readArray(reader);
case DICT_TYPE:
return readDict(reader);
case STRING_TYPE:
return readString(reader);
case INTEGER_TYPE:
return readInteger(reader);
case TRUE_TYPE:
return consumeElement(reader, true);
case FALSE_TYPE:
return consumeElement(reader, false);
case REAL_TYPE:
return readReal(reader);
case DATA_TYPE:
return readData(reader);
case DATE_TYPE:
return readDate(reader);
}
throw new XMLStreamException("Unknown type: " + name);
}
private List<Object> readArray(XMLStreamReader reader) throws XMLStreamException {
return readList(reader);
}
private Map<String, Object> readDict(XMLStreamReader reader) throws XMLStreamException {
Map<String, Object> objects = new HashMap<>();
String key = null;
while (reader.hasNext()) {
int eventType = reader.next();
switch (eventType) {
case XMLStreamReader.START_ELEMENT:
String name = reader.getLocalName();
if (name.equals(KEY_TYPE)) {
if (key != null) {
throw new XMLStreamException("Missing value for key " + key);
} else {
key = readKey(reader);
}
} else {
if (key == null) {
throw new XMLStreamException("Missing key for value");
} else {
objects.put(key, readValue(reader));
key = null;
}
}
break;
case XMLStreamReader.END_ELEMENT:
return objects;
}
}
throw new XMLStreamException(PREMATURE_EOF);
}
private String readKey(XMLStreamReader reader) throws XMLStreamException {
return readCharacters(reader);
}
private long readInteger(XMLStreamReader reader) throws XMLStreamException {
String chars = readCharacters(reader);
try {
return Long.valueOf(chars);
} catch (NumberFormatException e) {
throw new XMLStreamException("Invalid integer " + chars);
}
}
private String readString(XMLStreamReader reader) throws XMLStreamException {
return readCharacters(reader);
}
private float readReal(XMLStreamReader reader) throws XMLStreamException {
String chars = readCharacters(reader);
try {
return Float.valueOf(chars);
} catch (NumberFormatException e) {
throw new XMLStreamException("Invalid float " + chars);
}
}
private <T> T consumeElement(XMLStreamReader reader, T value)
throws XMLStreamException {
while (reader.hasNext()) {
int eventType = reader.next();
switch (eventType) {
case XMLStreamReader.END_ELEMENT:
return value;
}
}
throw new XMLStreamException(PREMATURE_EOF);
}
private Instant readDate(XMLStreamReader reader) throws XMLStreamException {
String chars = readCharacters(reader);
try {
return Instant.parse(chars);
} catch (DateTimeParseException e) {
throw new XMLStreamException("Invalid date time " + chars, e);
}
}
private byte[] readData(XMLStreamReader reader) throws XMLStreamException {
String chars = readCharacters(reader);
return Base64.getDecoder().decode(Arrays
.stream(LINE_BREAK.split(chars))
.map(String::trim)
.collect(Collectors.joining()));
}
private String readCharacters(XMLStreamReader reader) throws XMLStreamException {
StringBuilder result = new StringBuilder();
while (reader.hasNext()) {
int eventType = reader.next();
switch (eventType) {
case XMLStreamReader.CHARACTERS:
case XMLStreamReader.CDATA:
int length = reader.getTextLength();
int offset = reader.getTextStart();
char[] chars = reader.getTextCharacters();
result.append(chars, offset, length);
break;
case XMLStreamReader.END_ELEMENT:
return result.toString();
}
}
throw new XMLStreamException(PREMATURE_EOF);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment