package com.robonobo.gui.itunes.windows; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.robonobo.common.concurrent.CatchingRunnable; import com.robonobo.core.api.model.Playlist; import com.robonobo.core.api.model.SharedTrack; import com.robonobo.core.api.model.User; import com.robonobo.core.itunes.ITunesService; public class WindowsITunesService extends ITunesService { static final String CSCRIPT = "cscript.exe"; // All scripts have both a .js and a .wsf file, plus the commonFuncs.js file private String[] scriptNames = { "addTrackToUserPlaylist", "createRoboFolder", "createUserFolder", "createUserPlaylist", "deleteUserFolder", "deleteUserPlaylist", "listTracksInUserPlaylist", "listUserFolders", "listUserPlaylists", "syncUserPlaylist", "listTracksInLibrary", "listAllPlaylists" }; private File scriptsDir; private boolean iTunesReady = false; public WindowsITunesService() { } public String getName() { return "Windows iTunes Service"; } @Override public void startup() throws Exception { // Don't init iTunes here, do it on-demand (improves startup time) } private synchronized void checkReady() throws IOException { if (!iTunesReady) { createScriptsDir(); copyScriptsIntoDir(scriptsDir); // TODO iTunes will pop open if it's not already done, so make sure // we get the focus back afterwards iTunesReady = true; } } /** * Makes the 'Robonobo' folder within iTunes if it doesn't exist */ private void createRobonoboFolder() throws IOException { runScript("createRoboFolder.wsf", null); } private void copyScriptsIntoDir(File itScriptsDir) throws IOException { copyScriptFromJar("commonFuncs.js", itScriptsDir); for (String scriptName : scriptNames) { copyScriptFromJar(scriptName + ".js", itScriptsDir); copyScriptFromJar(scriptName + ".wsf", itScriptsDir); } } private void createScriptsDir() { scriptsDir = new File(new File(getRobonobo().getHomeDir(), "scripts"), "iTunes"); if (!scriptsDir.exists()) scriptsDir.mkdirs(); } private List<String> runScript(String script, List<String> scriptArgs) throws IOException { List<String> result = new ArrayList<String>(); List<String> procArgs = new ArrayList<String>(); procArgs.add(CSCRIPT); procArgs.add(script); if (scriptArgs != null) procArgs.addAll(scriptArgs); ProcessBuilder pb = new ProcessBuilder(procArgs); pb.directory(scriptsDir); Process proc = pb.start(); String line; BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); while ((line = reader.readLine()) != null) { result.add(line); } // Thread t = new Thread(new LogReader(proc.getInputStream())); // t.start(); // Wait for it to finish, then check the return status is ok int retCode; try { retCode = proc.waitFor(); } catch (InterruptedException e) { log.error("Process was interrupted: " + script); return result; } if (retCode != 0) { BufferedReader errReader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); String errLine; List<String> errLines = new ArrayList<String>(); while ((errLine = errReader.readLine()) != null) { errLines.add(errLine); } log.error("Error executing script '" + script + "': return code was " + retCode); return result; } return result; } private void copyScriptFromJar(String scriptName, File targetDir) throws IOException { String fqName = "com/robonobo/gui/itunes/windows/" + scriptName; InputStream is = getClass().getClassLoader().getResourceAsStream(fqName); OutputStream os = new FileOutputStream(new File(targetDir, scriptName)); byte[] buf = new byte[1024]; int bytesRead; while ((bytesRead = is.read(buf)) >= 0) { os.write(buf, 0, bytesRead); } is.close(); os.close(); } @Override public void shutdown() throws Exception { // Do nothing } @Override public List<File> getAllITunesFiles(FileFilter filter) throws IOException { checkReady(); List<File> result = new ArrayList<File>(); List<String> trackPaths = runScript("listTracksInLibrary.wsf", null); for (String trackPath : trackPaths) { File f = new File(trackPath); if (filter == null || filter.accept(f)) result.add(f); } return result; } @Override public Map<String, List<File>> getAllITunesPlaylists(FileFilter filter) throws IOException { checkReady(); Map<String, List<File>> result = new HashMap<String, List<File>>(); List<String> scOutput = runScript("listAllPlaylists.wsf", null); // This returns the playlist title on its own line, then the fq path of each track in the playlist on its own line, then a blank line... and repeat String playlistName = null; List<File> tracks = null; for (String line : scOutput) { if(playlistName == null) { playlistName = line; tracks = new ArrayList<File>(); continue; } if(line.trim().length() == 0) { if(tracks.size() > 0) result.put(playlistName, tracks); playlistName = null; continue; } File track = new File(line); if(track.exists() && ((filter == null) || filter.accept(track))) tracks.add(track); } return result; } @Override public synchronized void syncPlaylist(User u, Playlist p) throws IOException { checkReady(); createRobonoboFolder(); log.debug("Syncing with iTunes: user " + u.getEmail() + ", playlist '" + p.getTitle() + "'"); // First, check to see if we have a folder for the user String folderName = getFolderName(u); List<String> userFolders = runScript("listUserFolders.wsf", null); boolean gotFolder = false; for (String folder : userFolders) { if (folder.equals(folderName)) { gotFolder = true; break; } } // If not, create it if (!gotFolder) { log.info("Adding to iTunes: user " + u.getEmail() + ", playlist '" + p.getTitle() + "'"); List<String> args = new ArrayList<String>(); args.add(folderName); runScript("createUserFolder.wsf", args); } // Now check to see if the playlist exists List<String> args = new ArrayList<String>(); args.add(folderName); List<String> playlists = runScript("listUserPlaylists.wsf", args); boolean gotPlaylist = false; for (String playlistTitle : playlists) { if (playlistTitle.equals(p.getTitle())) { gotPlaylist = true; break; } } // Create the playlist if necessary if (!gotPlaylist) { List<String> createPlArgs = new ArrayList<String>(); createPlArgs.add(folderName); createPlArgs.add(p.getTitle()); runScript("createUserPlaylist.wsf", createPlArgs); } // Create a temp file with a list of the shared tracks in this playlist File trackListFile = File.createTempFile("robotracklist", "tmp"); trackListFile.deleteOnExit(); PrintWriter writer = new PrintWriter(trackListFile); for (String streamId : p.getStreamIds()) { SharedTrack sh = getRobonobo().getDbService().getShare(streamId); if (sh != null) { writer.println(sh.getFile().getAbsolutePath()); } } writer.close(); // Let's sync this sucker List<String> syncArgs = new ArrayList<String>(); syncArgs.add(folderName); syncArgs.add(p.getTitle()); syncArgs.add(trackListFile.getAbsolutePath()); runScript("syncUserPlaylist.wsf", syncArgs); } private String getFolderName(User u) { return u.getFriendlyName(); } class LogReader extends CatchingRunnable { InputStream is; public LogReader(InputStream is) { this.is = is; } @Override public void doRun() throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(is)); String line; while ((line = br.readLine()) != null) { log.debug("DEBUG: script output: " + line); } } } }