package com.sleazyweasel.applescriptifier; import com.google.gson.Gson; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.text.Collator; import java.util.*; public class AirfoilServlet extends HttpServlet { private static final String CURRENT_SOURCE_KEY = "currentSource"; private static final String NAME_KEY = "name"; private static final String STATE_KEY = "state"; private AppleScriptTemplate appleScriptTemplate = new AppleScriptTemplateFactory().getActiveTemplate(); private final MusicPlayer musicPlayer; public AirfoilServlet(MusicPlayer musicPlayer) { this.musicPlayer = musicPlayer; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException { // Set to expire far in the past. response.setHeader("Expires", "Sat, 6 May 1995 12:00:00 GMT"); // Set standard HTTP/1.1 no-cache headers. response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); // Set IE extended HTTP/1.1 no-cache headers (use addHeader). response.addHeader("Cache-Control", "post-check=0, pre-check=0"); // Set standard HTTP/1.0 no-cache header. response.setHeader("Pragma", "no-cache"); response.setContentType("application/json; charset=utf-8"); response.setCharacterEncoding("UTF-8"); String pathInfo = req.getPathInfo(); if (pathInfo.equals("/status")) { response.setStatus(HttpServletResponse.SC_OK); appendStatus(response); } else if (pathInfo.startsWith("/updateSpeaker")) { String speakerId = req.getParameter("id"); String conn = req.getParameter("conn"); String volume = req.getParameter("volume"); if (conn != null) { boolean connected = "1".equals(conn); updateSpeakerStatus(response, speakerId, connected); } else if (volume != null) { float vol = Float.parseFloat(volume); updateSpeakerVolume(response, speakerId, vol); } } else if (pathInfo.startsWith("/startApp")) { startApplication(response); } else if (pathInfo.startsWith("/selectApplicationAudioSource")) { String sourceId = req.getParameter("id"); selectApplicationAudioSource(sourceId, response); } else if (pathInfo.startsWith("/playpause")) { Map<String, Object> runningStatus = getRunningStatus(); ApplicationSupport applicationSupport = getCurrentApplicationSupport(runningStatus, musicPlayer); if (applicationSupport != null) { applicationSupport.playPause(); } appendRunningStatus(response, runningStatus); } else if (pathInfo.startsWith("/next")) { Map<String, Object> runningStatus = getRunningStatus(); ApplicationSupport applicationSupport = getCurrentApplicationSupport(runningStatus, musicPlayer); if (applicationSupport != null) { applicationSupport.next(); } appendRunningStatus(response, runningStatus); } else if (pathInfo.startsWith("/previous")) { Map<String, Object> runningStatus = getRunningStatus(); ApplicationSupport applicationSupport = getCurrentApplicationSupport(runningStatus, musicPlayer); if (applicationSupport != null) { applicationSupport.previous(); } appendRunningStatus(response, runningStatus); } else if (pathInfo.startsWith("/thumbsup")) { Map<String, Object> runningStatus = getRunningStatus(); ApplicationSupport applicationSupport = getCurrentApplicationSupport(runningStatus, musicPlayer); if (applicationSupport != null) { applicationSupport.thumbsUp(); } appendRunningStatus(response, runningStatus); } else if (pathInfo.startsWith("/thumbsdown")) { Map<String, Object> runningStatus = getRunningStatus(); ApplicationSupport applicationSupport = getCurrentApplicationSupport(runningStatus, musicPlayer); if (applicationSupport != null) { applicationSupport.thumbsDown(); } appendRunningStatus(response, runningStatus); } else if (pathInfo.startsWith("/bounce")) { bounceAirfoil(); appendRunningStatus(response, getRunningStatus()); } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } private void bounceAirfoil() { appleScriptTemplate.execute(Application.AIRFOIL(), "disconnect from every speaker"); appleScriptTemplate.execute(Application.AIRFOIL(), "quit"); String[] script = { "delay 1", "activate application \"Airfoil\"" }; appleScriptTemplate.executeBare(script); } private ApplicationSupport getCurrentApplicationSupport(Map<String, Object> runningStatus, MusicPlayer musicPlayer) { Map<String, Object> state = (Map<String, Object>) runningStatus.get(STATE_KEY); Map<String, Object> currentSource = (Map<String, Object>) state.get(CURRENT_SOURCE_KEY); String sourceName = (String) currentSource.get(NAME_KEY); Application currentApplication = Application.forName(sourceName); return currentApplication.getApplicationSupport(appleScriptTemplate, musicPlayer); } private void selectApplicationAudioSource(String sourceId, HttpServletResponse response) throws IOException { appleScriptTemplate.execute(Application.AIRFOIL(), "try", " set current audio source to item 1 of (every application source whose id is " + sourceId + ")", "on error", " set current audio source to item 1 of (every system source whose id is " + sourceId + ")", "end try"); appendRunningStatus(response, getRunningStatus()); } private void appendRunningStatus(HttpServletResponse response, Map<String, Object> runningStatus) throws IOException { response.getWriter().append(new Gson().toJson(runningStatus)); } private void startApplication(HttpServletResponse response) throws IOException { appleScriptTemplate.startApplication(Application.AIRFOIL()); appendRunningStatus(response, getRunningStatus()); } private void updateSpeakerVolume(HttpServletResponse response, String speakerId, float vol) throws IOException { String setScript = "set the volume of (every speaker whose id is \"" + speakerId + "\") to " + vol + ""; appleScriptTemplate.execute(Application.AIRFOIL(), setScript); appendRunningStatus(response, getRunningStatus()); } private void updateSpeakerStatus(HttpServletResponse response, String speakerId, boolean connect) throws IOException { if (connect) { appleScriptTemplate.execute(Application.AIRFOIL(), "connect to item 1 of (every speaker whose id is \"" + speakerId + "\")"); } else { appleScriptTemplate.execute(Application.AIRFOIL(), "disconnect from item 1 of (every speaker whose id is \"" + speakerId + "\")"); } appendRunningStatus(response, getRunningStatus()); } private void appendStatus(HttpServletResponse response) throws IOException { boolean isRunning = appleScriptTemplate.isRunning(Application.AIRFOIL()); if (!isRunning) { response.getWriter().append("{\"app\":\"").append(Application.AIRFOIL().name()).append("\",\"state\":{\"running\":false}}"); } else { appendRunningStatus(response, getRunningStatus()); } response.setStatus(HttpServletResponse.SC_OK); } private Map<String, Object> getRunningStatus() throws IOException { Map<String, Object> output = new HashMap<String, Object>(); output.put("app", Application.AIRFOIL().name()); Map<String, Object> stateMap = new HashMap<String, Object>(); stateMap.put("running", true); //todo this blows up if there is no "current audio source"... List data = appleScriptTemplate.execute(Application.AIRFOIL(), "[get properties of every speaker, get properties of every application source, get properties of every system source, get properties of current audio source]"); stateMap.put("speakers", data.get(0)); List<Map<String, Object>> sources = (List<Map<String, Object>>) data.get(1); sources.addAll((List) data.get(2)); stateMap.put("appSources", sources); Collections.sort(sources, new Comparator<Map<String, Object>>() { public int compare(Map<String, Object> o1, Map<String, Object> o2) { Collator collator = Collator.getInstance(); String name1 = (String) o1.get(NAME_KEY); String name2 = (String) o2.get(NAME_KEY); return collator.compare(name1, name2); } }); Map<String, Object> currentSource = (Map<String, Object>) data.get(3); String sourceName = (String) currentSource.get(NAME_KEY); Application currentApplication = Application.forName(sourceName); currentSource.put("playPause", currentApplication.playPauseSupport()); currentSource.put("previous", currentApplication.previousSupport()); currentSource.put("next", currentApplication.nextSupport()); currentSource.put("thumbsUp", currentApplication.thumbsUpSupport()); currentSource.put("thumbsDown", currentApplication.thumbsDownSupport()); stateMap.put(CURRENT_SOURCE_KEY, currentSource); stateMap.put("version", ControlServlet.CURRENT_VERSION); output.put(STATE_KEY, stateMap); return output; } }