package net.pms.network;
import com.sun.net.httpserver.*;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.*;
import net.pms.PMS;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.configuration.WebRender;
import net.pms.remote.RemoteUtil;
import net.pms.remote.RemoteWeb;
import net.pms.util.BasicPlayer.Logical;
import net.pms.util.StringUtil;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PlayerControlHandler implements HttpHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PlayerControlHandler.class);
private static final PmsConfiguration configuration = PMS.getConfiguration();
private int port;
private String protocol;
private RemoteWeb parent = null;
private HashMap<String, Logical> players;
private HashMap<InetAddress, Logical> selectedPlayers;
private String bumpAddress;
private RendererConfiguration defaultRenderer;
private String jsonState = "\"state\":{\"playback\":%d,\"mute\":\"%s\",\"volume\":%d,\"position\":\"%s\",\"duration\":\"%s\",\"uri\":\"%s\"}";
@SuppressWarnings("unused")
private File bumpjs, skindir;
public PlayerControlHandler(RemoteWeb web) {
this(web.getServer());
parent = web;
}
public PlayerControlHandler(HttpServer server) {
if (server == null) {
server = createServer(9009);
}
server.createContext("/bump", this);
port = server.getAddress().getPort();
protocol = server instanceof HttpsServer ? "https://" : "http://";
players = new HashMap<>();
selectedPlayers = new HashMap<>();
String basepath = configuration.getWebPath().getPath();
bumpjs = new File(FilenameUtils.concat(basepath, configuration.getBumpJS("bump/bump.js")));
skindir = new File(FilenameUtils.concat(basepath, configuration.getBumpSkinDir("bump/skin")));
bumpAddress = configuration.getBumpAddress();
defaultRenderer = null;
}
@Override
public void handle(HttpExchange x) throws IOException {
if (RemoteUtil.deny(x) && !RemoteUtil.bumpAllowed(x)) {
LOGGER.debug("Deny " + x);
throw new IOException("Denied");
}
String[] p = x.getRequestURI().getPath().split("/");
Map<String, String> q = parseQuery(x);
String response = "";
String mime = "text/html";
boolean log = true;
ArrayList<String> json = new ArrayList<>();
String uuid = p.length > 3 ? p[3] : null;
Logical player = uuid != null ? getPlayer(uuid) : null;
if (player != null) {
switch (p[2]) {
case "status":
// limit status updates to one per second
UPNPHelper.sleep(1000);
log = false;
break;
case "play":
player.pressPlay(translate(q.get("uri")), q.get("title"));
break;
case "stop":
player.pressStop();
break;
case "prev":
player.prev();
break;
case "next":
player.next();
break;
case "fwd":
player.forward();
break;
case "rew":
player.rewind();
break;
case "mute":
player.mute();
break;
case "setvolume":
player.setVolume(Integer.valueOf(q.get("vol")));
break;
case "add":
player.add(-1, translate(q.get("uri")), q.get("title"), null, true);
break;
case "remove":
player.remove(translate(q.get("uri")));
break;
case "clear":
player.clear();
break;
case "seturi":
player.setURI(translate(q.get("uri")), q.get("title"));
break;
}
json.add(getPlayerState(player));
json.add(getPlaylist(player));
selectedPlayers.put(x.getRemoteAddress().getAddress(), player);
} else if (p.length == 2) {
response = parent.getResources().read("bump/bump.html")
.replace("http://127.0.0.1:9001", protocol + PMS.get().getServer().getHost() + ":" + port);
} else if (p[2].equals("bump.js")) {
response = getBumpJS();
mime = "text/javascript";
} else if (p[2].equals("renderers")) {
json.add(getRenderers(x.getRemoteAddress().getAddress()));
} else if (p[2].startsWith("skin.")) {
RemoteUtil.dumpFile(new File(skindir, p[2].substring(5)), x);
return;
}
if (json.size() > 0) {
if (player != null) {
json.add("\"uuid\":\"" + uuid + "\"");
}
response = "{" + StringUtils.join(json, ",") + "}";
}
if (log) {
LOGGER.debug("Received http player control request from " + x.getRemoteAddress().getAddress() + ": " + x.getRequestURI());
}
Headers headers = x.getResponseHeaders();
headers.add("Content-Type", mime);
// w/o this client may receive response status 0 and no content
headers.add("Access-Control-Allow-Origin", "*");
byte[] bytes = response.getBytes();
x.sendResponseHeaders(200, bytes.length);
try (OutputStream o = x.getResponseBody()) {
o.write(bytes);
}
}
public String getAddress() {
return PMS.get().getServer().getHost() + ":" + port;
}
public Logical getPlayer(String uuid) {
Logical player = players.get(uuid);
if (player == null) {
try {
RendererConfiguration r = RendererConfiguration.getRendererConfigurationByUUID(uuid);
player = (Logical)r.getPlayer();
players.put(uuid, player);
} catch (Exception e) {
LOGGER.debug("Error retrieving player " + uuid + ": " + e);
}
}
return player;
}
public String getPlayerState(Logical player) {
if (player != null) {
Logical.State state = player.getState();
return String.format(jsonState, state.playback, state.mute, state.volume, StringUtil.shortTime(state.position, 4), StringUtil.shortTime(state.duration, 4), state.uri/*, state.metadata*/);
}
return "";
}
public RendererConfiguration getDefaultRenderer() {
if (defaultRenderer == null && bumpAddress != null) {
try {
InetAddress ia = InetAddress.getByName(bumpAddress);
defaultRenderer = RendererConfiguration.getRendererConfigurationBySocketAddress(ia);
} catch (UnknownHostException e) {
}
}
return (defaultRenderer != null && !defaultRenderer.isOffline()) ? defaultRenderer : null;
}
public String getRenderers(InetAddress client) {
Logical player = selectedPlayers.get(client);
RendererConfiguration selected = player != null ? player.renderer : getDefaultRenderer();
ArrayList<String> json = new ArrayList();
for (RendererConfiguration r : RendererConfiguration.getConnectedControlPlayers()) {
json.add(String.format("[\"%s\",%d,\"%s\"]", (r instanceof WebRender) ? r.uuid : r, r == selected ? 1 : 0, r.uuid));
}
return "\"renderers\":[" + StringUtils.join(json, ",") + "]";
}
public String getPlaylist(Logical player) {
ArrayList<String> json = new ArrayList();
Logical.Playlist playlist = player.playlist;
playlist.validate();
Logical.Playlist.Item selected = (Logical.Playlist.Item) playlist.getSelectedItem();
int i;
for (i = 0; i < playlist.getSize(); i++) {
Logical.Playlist.Item item = (Logical.Playlist.Item) playlist.getElementAt(i);
json.add(String.format("[\"%s\",%d,\"%s\"]",
item.toString().replace("\"", "\\\""), item == selected ? 1 : 0, "$i$" + i));
}
return "\"playlist\":[" + StringUtils.join(json, ",") + "]";
}
public String getBumpJS() {
RemoteUtil.ResourceManager resources = parent.getResources();
return resources.read("bump/bump.js")
+ "\nvar bumpskin = function() {\n"
+ resources.read("bump/skin/skin.js")
+ "\n}";
}
public static Map<String, String> parseQuery(HttpExchange x) {
Map<String, String> vars = new LinkedHashMap<>();
String raw = x.getRequestURI().getRawQuery();
if (!StringUtils.isBlank(raw)) {
try {
String[] q = raw.split("&|=");
for (int i = 0; i < q.length; i += 2) {
vars.put(URLDecoder.decode(q[i], "UTF-8"), UPNPHelper.unescape(URLDecoder.decode(q[i + 1], "UTF-8")));
}
} catch (Exception e) {
LOGGER.debug("Error parsing query string '" + x.getRequestURI().getQuery() + "' :" + e);
}
}
return vars;
}
public static String translate(String uri) {
return uri.startsWith("/play/")
? (PMS.get().getServer().getURL() + "/get/" + uri.substring(6).replace("%24", "$")) : uri;
}
@SuppressWarnings("unused")
private String getId(String uri) {
return uri.startsWith("/play/") ? uri.substring(6) : "";
}
// For standalone service, if required
private static HttpServer createServer(int socket) {
HttpServer server = null;
try {
server = HttpServer.create(new InetSocketAddress(socket), 0);
server.start();
} catch (IOException e) {
LOGGER.debug("Error creating bump server: " + e);
}
return server;
}
}