package com.atomjack.vcfp.model; import android.os.Parcel; import android.os.Parcelable; import com.atomjack.shared.Logger; import com.atomjack.shared.Preferences; import com.atomjack.vcfp.PlexHeaders; import com.atomjack.vcfp.QueryString; import com.atomjack.vcfp.R; import com.atomjack.vcfp.Utils; import com.atomjack.vcfp.VoiceControlForPlexApplication; import com.atomjack.vcfp.interfaces.ActiveConnectionHandler; import com.atomjack.vcfp.interfaces.AfterTransientTokenRequest; import com.atomjack.vcfp.interfaces.ServerTestHandler; import com.atomjack.vcfp.net.PlexHttpClient; import com.atomjack.vcfp.net.PlexHttpMediaContainerHandler; import com.atomjack.vcfp.net.PlexHttpResponseHandler; import com.google.gson.reflect.TypeToken; import org.simpleframework.xml.Root; import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import retrofit.Call; import retrofit.Callback; import retrofit.Response; import retrofit.Retrofit; @Root(strict=false) public class PlexServer extends PlexDevice { public String sourceTitle; public List<String> movieSections = new ArrayList<String>(); public List<String> tvSections = new ArrayList<String>(); public List<String> musicSections = new ArrayList<String>(); public int movieSectionsSearched = 0; public int tvSectionsSearched = 0; public int musicSectionsSearched = 0; public boolean owned = true; public String accessToken; public boolean local; public boolean isScanAllServer = false; public PlexServer() { super(); connections = new ArrayList<Connection>(); } public PlexServer(String _name) { super(); name = _name; connections = new ArrayList<Connection>(); } public static PlexServer getScanAllServer() { PlexServer s = new PlexServer(VoiceControlForPlexApplication.getInstance().getString(R.string.scan_all)); s.machineIdentifier = "000000"; s.isScanAllServer = true; return s; } public static PlexServer fromDevice(Device device) { Logger.d("Creating server %s", device.name); PlexServer server = new PlexServer(device.name); server.sourceTitle = device.sourceTitle; server.connections = device.connections; server.address = ""; if(!device.owned) { Logger.d("Not owned, has %d connections.", device.connections.size()); for(Connection connection : device.connections) { Logger.d("Connection %s: %s", connection.address, connection.local); if(!connection.local) { server.address = connection.address; server.port = connection.port; break; } } } else { if(server.connections != null && server.connections.size() > 0) { server.address = server.connections.get(0).address; server.port = server.connections.get(0).port; } } server.machineIdentifier = device.clientIdentifier; server.owned = device.owned; server.product = device.product; server.accessToken = device.accessToken; return server; } public void addMovieSection(String key) { if(!movieSections.contains(key)) { movieSections.add(key); } } public void addTvSection(String key) { if(!tvSections.contains(key)) { tvSections.add(key); } } public void addMusicSection(String key) { if(!musicSections.contains(key)) { musicSections.add(key); } } @Override public String toString() { String output = ""; output += "Name: " + name + "\n"; output += "IP Address: " + address + "\n"; if(connections != null) { output += "Connections: \n"; for (Connection c : connections) { output += String.format("%s (%s)\n", c.uri, c.local); } } return output; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeString(name); parcel.writeString(port); parcel.writeString(version); parcel.writeString(product); parcel.writeString(address); parcel.writeString(accessToken); parcel.writeString(machineIdentifier); parcel.writeTypedList(connections); parcel.writeStringList(movieSections); parcel.writeStringList(tvSections); parcel.writeStringList(musicSections); parcel.writeInt(owned ? 1 : 0); parcel.writeString(sourceTitle); } public PlexServer(Parcel in) { this(); name = in.readString(); port = in.readString(); version = in.readString(); product = in.readString(); address = in.readString(); accessToken = in.readString(); machineIdentifier = in.readString(); in.readTypedList(connections, Connection.CREATOR); in.readStringList(movieSections); in.readStringList(tvSections); in.readStringList(musicSections); owned = in.readInt() == 1; sourceTitle = in.readString(); } public static final Parcelable.Creator<PlexServer> CREATOR = new Parcelable.Creator<PlexServer>() { public PlexServer createFromParcel(Parcel in) { return new PlexServer(in); } public PlexServer[] newArray(int size) { return new PlexServer[size]; } }; public void findServerConnection(final ActiveConnectionHandler activeConnectionHandler) { findServerConnection(false, activeConnectionHandler); } // force == ignore active connection and scan for all connections. Defaults to false (use activeConnection, if it exists) public void findServerConnection(boolean force, final ActiveConnectionHandler activeConnectionHandler) { Calendar activeConnectionExpires = null; Connection activeConnection = null; if(!force) { try { HashMap<String, Calendar> activeConnectionExpiresList = VoiceControlForPlexApplication.getInstance().getActiveConnectionExpiresList(); HashMap<String, Connection> activeConnectionList = VoiceControlForPlexApplication.getInstance().getActiveConnectionList(); if (activeConnectionExpiresList.containsKey(machineIdentifier)) activeConnectionExpires = activeConnectionExpiresList.get(machineIdentifier); if (activeConnectionList.containsKey(machineIdentifier)) activeConnection = activeConnectionList.get(machineIdentifier); } catch (Exception e) { e.printStackTrace(); } } // If we have an active connection and it has not expired yet, AND (the active connection is on the same network OR it is an external IP), // return the active connection. Otherwise, scan for them. // If the active connection is an external IP, we obviously don't care if it's on a different network. if(activeConnectionExpires != null && !activeConnectionExpires.before(Calendar.getInstance()) && (activeConnection.isOnSameNetwork() || !activeConnection.isPrivateV4Address())) { activeConnectionHandler.onSuccess(activeConnection); } else { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEEE, MMMM d, yyyy 'at' h:mm:ss a"); logger.d("finding server connection for %s, current active connection expires: %s, now: %s", name, activeConnectionExpires != null ? simpleDateFormat.format(activeConnectionExpires.getTime()) : null, simpleDateFormat.format(Calendar.getInstance().getTime())); findServerConnection(0, activeConnectionHandler); } } private void findServerConnection(final int connectionIndex, final ActiveConnectionHandler activeConnectionHandler) { final Connection connection = connections.get(connectionIndex); // If this connection is a lan IP address and is not on the same subnet as the device, skip testing this connection logger.d("this ip address: %s, connection ip: %s", Utils.getIPAddress(true), connection.address); if(connection.isPrivateV4Address() && !connection.isOnSameNetwork()) { logger.d("Connection %s for server %s is private but not on the same network. Skipping.", connection.address, name); int newConnectionIndex = connectionIndex + 1; if (connections.size() <= newConnectionIndex) { activeConnectionHandler.onFailure(0); } else findServerConnection(newConnectionIndex, activeConnectionHandler); return; } testServerConnection(connection, (statusCode, available) -> { if (available) { // This connection replied, so let's use it Connection activeConnection = connections.get(connectionIndex); Logger.d("Found connection for %s: %s", name, activeConnection); Calendar activeConnectionExpires = Calendar.getInstance(); activeConnectionExpires.add(Calendar.HOUR_OF_DAY, 1); // Now save this connection and expiration VoiceControlForPlexApplication.getInstance().saveActiveConnection(PlexServer.this, activeConnection, activeConnectionExpires); VoiceControlForPlexApplication.servers.put(name, PlexServer.this); Type serverType = new TypeToken<ConcurrentHashMap<String, PlexServer>>(){}.getType(); VoiceControlForPlexApplication.getInstance().prefs.put(Preferences.SAVED_SERVERS, VoiceControlForPlexApplication.gsonWrite.toJson(VoiceControlForPlexApplication.servers, serverType)); activeConnectionHandler.onSuccess(activeConnection); } else { int newConnectionIndex = connectionIndex + 1; Logger.d("Not available, new connection index: %d", newConnectionIndex); if (connections.size() <= newConnectionIndex) { activeConnectionHandler.onFailure(statusCode); } else findServerConnection(newConnectionIndex, activeConnectionHandler); } }); } private void testServerConnection(final Connection connection, final ServerTestHandler handler) { PlexHttpClient.PlexHttpService service = PlexHttpClient.getService(connection, 2); Call<MediaContainer> call = service.getMediaContainer("", accessToken); call.enqueue(new Callback<MediaContainer>() { @Override public void onResponse(Response<MediaContainer> response, Retrofit retrofit) { Logger.d("Testing connection %s got code: %d", connection, response.code()); if(response.code() == 200) { Logger.d("%s success", connection.uri); handler.onFinish(response.code(), true); } else { handler.onFinish(response.code(), false); } } @Override public void onFailure(Throwable t) { t.printStackTrace(); Logger.d("%s failed", connection.uri); handler.onFinish(0, false); } }); } public void requestTransientAccessToken(final AfterTransientTokenRequest onFinish) { String path = "/security/token?type=delegation&scope=all"; PlexHttpClient.get(this, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { onFinish.success(mediaContainer.token); } @Override public void onFailure(Throwable error) { error.printStackTrace(); onFinish.failure(); } }); } public String buildURL(Connection connection, String path) { String url = String.format("%s%s", connection.uri, path); if(accessToken != null) url += String.format("%s%s=%s", (url.contains("?") ? "&" : "?"), PlexHeaders.XPlexToken, accessToken); return url; } public void localPlay(PlexMedia media, boolean resumePlayback, String transientToken) { localPlay(media, resumePlayback, null, transientToken); } public void localPlay(PlexMedia media, String containerKey, boolean resumePlayback) { localPlay(media, resumePlayback, containerKey, null); } public void localPlay(final PlexMedia media, final boolean resumePlayback, final String containerKey, final String transientToken) { findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { QueryString qs = new QueryString("machineIdentifier", machineIdentifier); Logger.d("machine id: %s", machineIdentifier); qs.add("key", media.key); Logger.d("key: %s", media.key); qs.add("port", connection.port); Logger.d("port: %s", connection.port); qs.add("address", connection.address); Logger.d("address: %s", connection.address); if(containerKey != null) qs.add("containerKey", containerKey); if((VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false) || resumePlayback) && media.viewOffset != null) qs.add("viewOffset", media.viewOffset); if(transientToken != null) qs.add("token", transientToken); if(accessToken != null) qs.add(PlexHeaders.XPlexToken, accessToken); String url = String.format("http://127.0.0.1:32400/player/playback/playMedia?%s", qs); logger.d("Playback url: %s ", url); PlexHttpClient.get("http://127.0.0.1:32400", String.format("player/playback/playMedia?%s", qs), new PlexHttpResponseHandler() { @Override public void onSuccess(PlexResponse r) { } @Override public void onFailure(Throwable error) { // feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } @Override public void onFailure(int statusCode) { } }); } public String getRandomMusicSection() { return musicSections.get(new Random().nextInt(musicSections.size())); } public String getRandomTvSection() { return tvSections.get(new Random().nextInt(tvSections.size())); } public String getRandomMovieSection() { return movieSections.get(new Random().nextInt(movieSections.size())); } }