package com.atomjack.vcfp.services; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.speech.RecognizerIntent; import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import com.atomjack.shared.NewLogger; import com.atomjack.shared.Preferences; import com.atomjack.shared.SendToDataLayerThread; import com.atomjack.shared.WearConstants; import com.atomjack.vcfp.BuildConfig; import com.atomjack.vcfp.Feedback; import com.atomjack.vcfp.FetchMediaImageTask; import com.atomjack.vcfp.LimitedAsyncTask; import com.atomjack.vcfp.QueryString; import com.atomjack.vcfp.R; import com.atomjack.vcfp.Utils; import com.atomjack.vcfp.VoiceControlForPlexApplication; import com.atomjack.vcfp.activities.MainActivity; import com.atomjack.vcfp.activities.VideoPlayerActivity; import com.atomjack.vcfp.interfaces.ActiveConnectionHandler; import com.atomjack.vcfp.interfaces.AfterTransientTokenRequest; import com.atomjack.vcfp.interfaces.BitmapHandler; import com.atomjack.vcfp.interfaces.PlexPlayQueueHandler; import com.atomjack.vcfp.model.Connection; import com.atomjack.vcfp.model.MediaContainer; import com.atomjack.vcfp.model.PlexClient; import com.atomjack.vcfp.model.PlexDirectory; import com.atomjack.vcfp.model.PlexMedia; import com.atomjack.vcfp.model.PlexResponse; import com.atomjack.vcfp.model.PlexServer; import com.atomjack.vcfp.model.PlexTrack; import com.atomjack.vcfp.model.PlexVideo; import com.atomjack.vcfp.model.Stream; import com.atomjack.vcfp.net.PlexHttpClient; import com.atomjack.vcfp.net.PlexHttpMediaContainerHandler; import com.atomjack.vcfp.net.PlexHttpResponseHandler; import com.google.android.gms.cast.Cast; import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.wearable.DataMap; import com.splunk.mint.Mint; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PlexSearchService extends Service implements ServiceConnection { private NewLogger logger; private String queryText; private SearchFeedback feedback; private ConcurrentHashMap<String, PlexServer> plexmediaServers = new ConcurrentHashMap<>(); private Map<String, PlexClient> clients; private PlexClient client = null; private PlexServer specifiedServer = null; private int serversSearched = 0; private List<PlexVideo> videos = new ArrayList<>(); private Boolean videoPlayed = false; private List<PlexDirectory> shows = new ArrayList<>(); private Boolean resumePlayback = false; private List<PlexTrack> tracks = new ArrayList<>(); private List<PlexDirectory> albums = new ArrayList<>(); private boolean didClientScan = false; private int whichLocalPlayer = -1; // Which local player (audio/video) this is coming from private boolean shuffle = false; private List<String> queries; private boolean showPlayer = true; // whether or not to show the player when starting playback // Will be set to true after we scan for servers, so we don't have to do it again on the next query private boolean didServerScan = false; private MainActivity.NetworkState currentNetworkState; private boolean fromWear = false; private boolean fromGoogleNow = false; // Chromecast MediaRouter mMediaRouter; MediaRouterCallback mMediaRouterCallback; MediaRouteSelector mMediaRouteSelector; GoogleApiClient mApiClient; boolean mWaitingForReconnect = false; Cast.Listener mCastClientListener; ConnectionCallbacks mConnectionCallbacks; private SubscriptionService subscriptionService; private boolean subscriptionServiceIsBound = false; private Runnable subscriptionServiceOnConnected = () -> {}; // Callbacks for when we figure out what action the user wishes to take. private myRunnable actionToDo; private interface myRunnable { void run(); } // An instance of this interface will be returned by handleVoiceSearch when no server discovery is // needed (e.g. pause/resume/stop playback or offset) private interface StopRunnable extends myRunnable {} @Override public void onCreate() { logger = new NewLogger(this); logger.d("onCreate"); queryText = null; feedback = new SearchFeedback(this); Intent subscriptionServiceIntent = new Intent(getApplicationContext(), SubscriptionService.class); getApplicationContext().bindService(subscriptionServiceIntent, this, Context.BIND_AUTO_CREATE); getApplicationContext().startService(subscriptionServiceIntent); } @Override @SuppressWarnings("unchecked") public int onStartCommand(Intent intent, int flags, int startId) { logger.d("onStartCommand: %s", intent.getAction()); if(!subscriptionServiceIsBound) { subscriptionServiceOnConnected = () -> { onStartCommand(intent, flags, startId); }; return Service.START_NOT_STICKY; } // Reset whether we've scanned for clients, but only if we're getting here from a new voice search (we can // get here from scanning for clients) if(intent.getAction() == null || intent.getAction().equals(com.atomjack.shared.Intent.PLEX_SEARCH)) didClientScan = false; if(BuildConfig.USE_BUGSENSE) Mint.initAndStartSession(PlexSearchService.this, MainActivity.BUGSENSE_APIKEY); videoPlayed = false; shuffle = false; whichLocalPlayer = intent.getIntExtra(com.atomjack.shared.Intent.PLAYER, -1); if(intent.getBooleanExtra(WearConstants.FROM_WEAR, false) && VoiceControlForPlexApplication.getInstance().hasWear()) { fromWear = true; } fromGoogleNow = intent.getBooleanExtra(com.atomjack.shared.Intent.EXTRA_FROM_GOOGLE_NOW, false); showPlayer = intent.getBooleanExtra(com.atomjack.shared.Intent.SHOW_PLAYER, true); if(fromGoogleNow && !VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.GOOGLE_NOW_LAUNCH_NOW_PLAYING, true)) showPlayer = false; currentNetworkState = MainActivity.NetworkState.getCurrentNetworkState(this); logger.d("action: %s", intent.getAction()); logger.d("scan type: %s", intent.getStringExtra(com.atomjack.shared.Intent.SCAN_TYPE)); if(intent.getAction() != null && !intent.getAction().equals(com.atomjack.shared.Intent.PLEX_SEARCH)) { if (intent.getAction().equals(PlexScannerService.ACTION_SERVER_SCAN_FINISHED)) { // We just scanned for servers and are returning from that, so set the servers we found // and then figure out which client to play to logger.d("Got back from scanning for servers."); videoPlayed = false; HashMap<String, PlexServer> s = (HashMap<String, PlexServer>) intent.getSerializableExtra(com.atomjack.shared.Intent.EXTRA_SERVERS); VoiceControlForPlexApplication.servers = new ConcurrentHashMap<>(s); plexmediaServers = VoiceControlForPlexApplication.servers; didServerScan = true; setClient(); } else if (intent.getAction().equals(PlexScannerService.ACTION_CLIENT_SCAN_FINISHED)) { // Got back from client scan, so set didClientScan to true so we don't do this again, and save the clients we got, then continue didClientScan = true; ArrayList<PlexClient> cs = intent.getParcelableArrayListExtra(com.atomjack.shared.Intent.EXTRA_CLIENTS); if (cs != null) { VoiceControlForPlexApplication.clients = new HashMap<>(); for (PlexClient c : cs) { VoiceControlForPlexApplication.clients.put(c.name, c); } clients = VoiceControlForPlexApplication.getAllClients(); // clients = VoiceControlForPlexApplication.clients; // clients.putAll(VoiceControlForPlexApplication.castClients); } startup(); } } else { queryText = null; client = null; mMediaRouter = MediaRouter.getInstance(getApplicationContext()); mMediaRouteSelector = new MediaRouteSelector.Builder() .addControlCategory(CastMediaControlIntent.categoryForCast(BuildConfig.CHROMECAST_APP_ID)) .build(); mMediaRouterCallback = new MediaRouterCallback(); mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); mConnectionCallbacks = new ConnectionCallbacks(); mCastClientListener = new Cast.Listener() { @Override public void onApplicationStatusChanged() { if (mApiClient != null) { logger.d("onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(mApiClient)); } } @Override public void onVolumeChanged() { if (mApiClient != null) { logger.d("onVolumeChanged: " + Cast.CastApi.getVolume(mApiClient)); } } @Override public void onApplicationDisconnected(int errorCode) { // TODO: Teardown? //teardown(); } }; queries = new ArrayList<>(); clients = VoiceControlForPlexApplication.getAllClients(); resumePlayback = false; specifiedServer = VoiceControlForPlexApplication.gsonRead.fromJson(intent.getStringExtra(com.atomjack.shared.Intent.EXTRA_SERVER), PlexServer.class); if(specifiedServer != null) logger.d("specified server %s", specifiedServer); PlexClient thisClient = VoiceControlForPlexApplication.gsonRead.fromJson(intent.getStringExtra(com.atomjack.shared.Intent.EXTRA_CLIENT), PlexClient.class); if(thisClient != null) { client = thisClient; // logger.d("Got client from hardcoded shortcut, lastUpdated: %s.", client.lastUpdated); // See if this same client has been saved into settings more recently than the shortcut was created, and if so, use the saved client in case its IP address has changed for (PlexClient theClient : VoiceControlForPlexApplication.clients.values()) { if(theClient.machineIdentifier != null && theClient.machineIdentifier.equals(client.machineIdentifier)) { // logger.d("Found saved client, last updated: %s", theClient.lastUpdated); if(client.lastUpdated == null || (theClient.lastUpdated != null && theClient.lastUpdated.after(client.lastUpdated))) { logger.d("Saved client was updated after shortcut was created. Using saved client instead."); client = theClient; } } } } if(intent.getBooleanExtra(com.atomjack.shared.Intent.EXTRA_RESUME, false)) resumePlayback = true; if(intent.getBooleanExtra(com.atomjack.shared.Intent.USE_CURRENT, false)) { logger.d("Using current, setting resume playback to %s", VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false)); resumePlayback = VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false); } if (intent.getExtras().getStringArrayList(RecognizerIntent.EXTRA_RESULTS) != null) { logger.d("internal query"); // Received spoken query from the RecognizerIntent ArrayList<String> voiceResults = intent.getExtras().getStringArrayList(RecognizerIntent.EXTRA_RESULTS); for(String q : voiceResults) { if(q.toLowerCase().matches(getString(R.string.pattern_recognition))) { if(!queries.contains(q.toLowerCase())) queries.add(q.toLowerCase()); } } if(queries.size() == 0) { logger.d("Didn't understand query %s", intent.getExtras().getStringArrayList(RecognizerIntent.EXTRA_RESULTS)); feedback.e(getResources().getString(R.string.didnt_understand_that)); return Service.START_NOT_STICKY; } } else { // Received spoken query from Google Search API logger.d("Google Search API query"); queries.add(intent.getStringExtra(com.atomjack.shared.Intent.EXTRA_QUERYTEXT)); } if(client == null) { client = VoiceControlForPlexApplication.gsonRead.fromJson(VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.CLIENT, ""), PlexClient.class); logger.d("set client to %s", client); } if(client == null && didClientScan) { // No mClient set in options, and either none specified in the query or I just couldn't find it. feedback.e(getResources().getString(R.string.client_not_specified)); return Service.START_NOT_STICKY; } if(subscriptionService.isSubscribed()) { if(client != null && subscriptionService.getClient() != null && !client.machineIdentifier.equals(subscriptionService.getClient().machineIdentifier)) { logger.d("subscribed to a client but need to play on a different client"); subscriptionService.unsubscribe(); } } if (queries.size() > 0) { logger.d("Starting up, with queries: %s", queries); startup(); } else feedback.e(getResources().getString(R.string.didnt_understand_that)); } return Service.START_NOT_STICKY; } @Override public void onDestroy() { super.onDestroy(); feedback.destroy(); if(subscriptionServiceIsBound) { getApplicationContext().unbindService(this); subscriptionServiceIsBound = false; } } @Override public IBinder onBind(Intent intent) { logger.d(": onBind"); return null; } private void startup() { queryText = queries.remove(0); if(queryText.contains("-")) { queries.add(queryText.replaceAll("-", "")); } if(queryText.matches(getString(R.string.pattern_on_client)) && !queryText.matches(getString(R.string.pattern_whats_on_deck))) { Pattern p = Pattern.compile(getString(R.string.pattern_on_client), Pattern.DOTALL); Matcher matcher = p.matcher(queryText); matcher.find(); String specifiedClient = matcher.group(2).toLowerCase(); boolean found = false; for(PlexClient client : VoiceControlForPlexApplication.getInstance().getAllClients().values()) { if(client.name.toLowerCase().equals(specifiedClient)) { found = true; break; } } // Only scan for clients if we're not already aware of the client specified if(!found && !didClientScan) { // A client was specified in the query, so let's scan for clients before proceeding. // First, insert the query text back into the queries array, so we can use it after the scan is done. queries.add(0, queryText); sendClientScanIntent(); return; } } logger.d("Starting up with query string: %s", queryText); tracks = new ArrayList<>(); videos = new ArrayList<>(); shows = new ArrayList<>(); final PlexServer defaultServer = VoiceControlForPlexApplication.gsonRead.fromJson(VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.SERVER, ""), PlexServer.class); if(specifiedServer != null && client != null && !specifiedServer.name.equals(getResources().getString(R.string.scan_all))) { // got a specified server and client from a shortcut logger.d("Got hardcoded server and client from shortcut with %d music sections", specifiedServer.musicSections.size()); plexmediaServers = new ConcurrentHashMap<>(); // If the chosen server exists in the master list of servers, use that one as it will have the last time a connection scan was done if(VoiceControlForPlexApplication.servers.containsKey(specifiedServer.name)) plexmediaServers.put(specifiedServer.name, VoiceControlForPlexApplication.servers.get(specifiedServer.name)); else plexmediaServers.put(specifiedServer.name, specifiedServer); setClient(); } else if(specifiedServer == null && defaultServer != null && !defaultServer.name.equals(getResources().getString(R.string.scan_all))) { // Use the server specified in the main settings logger.d("Using server and client specified in main settings"); plexmediaServers = new ConcurrentHashMap<>(); // If the chosen server exists in the master list of servers, use that one as it will have the last time a connection scan was done if(VoiceControlForPlexApplication.servers.containsKey(defaultServer.name)) plexmediaServers.put(defaultServer.name, VoiceControlForPlexApplication.servers.get(defaultServer.name)); else plexmediaServers.put(defaultServer.name, defaultServer); setClient(); } else { // Scan All was chosen logger.d("Scan all was chosen, seconds since last server scan: %d", VoiceControlForPlexApplication.getInstance().getSecondsSinceLastServerScan()); if(didServerScan || VoiceControlForPlexApplication.getInstance().getSecondsSinceLastServerScan() <= (MainActivity.SERVER_SCAN_INTERVAL/ 1000) || !BuildConfig.AUTO_REFRESH_DEVICES) { // Set the media servers we will scan to the ones saved in the application. This will either just have been saved after a server scan, due to // the last server scan being more than 5 minutes ago, or else it will be what was already stored since it's been less than 5 minutes since the last // scan (or the app is the debug version which doesn't auto scan) plexmediaServers = VoiceControlForPlexApplication.servers; setClient(); return; } // First, see if what needs to be done actually needs to know about the server (i.e. pause/stop/resume playback or offset). // If it doesn't, execute the action and return as we don't need to do anything else. However, also check to see if the user // has specified a client (using " on <client name>") - if this is the case, we will need to find that client via server // discovery myRunnable actionToDo = handleVoiceSearch(true); if(actionToDo == null) { startup(); } else { if (actionToDo instanceof StopRunnable && (queryText.matches(getString(R.string.pattern_whats_on_deck)) || !queryText.matches(getString(R.string.pattern_on_client)))) { actionToDo.run(); return; } Intent scannerIntent = new Intent(PlexSearchService.this, PlexScannerService.class); scannerIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); scannerIntent.putExtra(PlexScannerService.CLASS, PlexSearchService.class); scannerIntent.setAction(PlexScannerService.ACTION_SCAN_SERVERS); startService(scannerIntent); feedback.m("Scanning for Plex Servers"); } } } private void setClient() { actionToDo = handleVoiceSearch(); if(actionToDo == null) { startup(); } else actionToDo.run(); } private myRunnable handleVoiceSearch() { return handleVoiceSearch(false); } private myRunnable handleVoiceSearch(boolean noChange) { logger.d("GOT QUERY: %s", queryText); Pattern p; Matcher matcher; if(!noChange) { p = Pattern.compile(getString(R.string.pattern_on_client), Pattern.DOTALL); matcher = p.matcher(queryText); Pattern p2 = Pattern.compile(getString(R.string.pattern_on_shuffle), Pattern.DOTALL); Matcher matcher2 = p2.matcher(queryText); if (matcher.find() && !matcher2.find()) { String specifiedClient = matcher.group(2).toLowerCase(); logger.d("Clients: %d", clients.size()); logger.d("Specified client: %s", specifiedClient); for(PlexClient c : clients.values()) { logger.d("comparing %s to %s", c.name.toLowerCase(), specifiedClient); if (c.name.toLowerCase().equals(specifiedClient)) { if(c.isCastClient && !VoiceControlForPlexApplication.getInstance().hasChromecast()) { return () -> feedback.e(R.string.must_purchase_chromecast_error); } else { client = c; queryText = queryText.replaceAll(getString(R.string.pattern_on_client), "$1"); logger.d("query text now %s", queryText); break; } } } } // Check for a sentence starting with "resume watching/playing" p = Pattern.compile(getString(R.string.pattern_resume_watching)); matcher = p.matcher(queryText); if(matcher.find()) { resumePlayback = true; // Replace "resume watching/playing" with just "watch" so the pattern matching below works queryText = matcher.replaceAll(getString(R.string.pattern_watch)); } // Check for a sentence ending with "on shuffle" p = Pattern.compile(getString(R.string.pattern_on_shuffle)); matcher = p.matcher(queryText); if(matcher.find()) { shuffle = true; // Remove "on shuffle" from the query text queryText = matcher.replaceAll("").trim(); logger.d("Shuffling, query is now !%s!", queryText); } else { logger.d("No shuffle"); } } // Done changing the query if the user said "resume watching", "on shuffled", or specified a client p = Pattern.compile( getString(R.string.pattern_watch_movie), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { final String queryTerm = matcher.group(2); return () -> doMovieSearch(queryTerm); } p = Pattern.compile(getString(R.string.pattern_watch_season_episode_of_show)); matcher = p.matcher(queryText); if(matcher.find()) { final String queryTerm = matcher.group(4); final String season = matcher.group(2); final String episode = matcher.group(3); return new myRunnable() { @Override public void run() { doShowSearch(queryTerm, season, episode); } }; } p = Pattern.compile(getString(R.string.pattern_watch_show_season_episode)); matcher = p.matcher(queryText); if(matcher.find()) { final String queryTerm = matcher.group(2); final String season = matcher.group(3); final String episode = matcher.group(4); return new myRunnable() { @Override public void run() { doShowSearch(queryTerm, season, episode); } }; } p = Pattern.compile(getString(R.string.pattern_watch_episode_of_show)); matcher = p.matcher(queryText); if(matcher.find()) { final String episodeSpecified = matcher.group(2); final String showSpecified = matcher.group(3); return new myRunnable() { @Override public void run() { doShowSearch(episodeSpecified, showSpecified); } }; } p = Pattern.compile(getString(R.string.pattern_watch_next_episode_of_show)); matcher = p.matcher(queryText); if(matcher.find()) { final String queryTerm = matcher.group(2); return new myRunnable() { @Override public void run() { doNextEpisodeSearch(queryTerm, false); } }; } p = Pattern.compile(getString(R.string.pattern_watch_latest_episode_of_show)); matcher = p.matcher(queryText); if(matcher.find()) { final String queryTerm = matcher.group(3); return new myRunnable() { @Override public void run() { doLatestEpisodeSearch(queryTerm); } }; } p = Pattern.compile(getString(R.string.pattern_random_episode)); matcher = p.matcher(queryText); if(matcher.find()) { final String showSpecified = matcher.group(3); return new myRunnable() { @Override public void run() { playRandomEpisode(showSpecified); } }; } p = Pattern.compile(getString(R.string.pattern_watch_show_episode_named)); matcher = p.matcher(queryText); if(matcher.find()) { final String episodeSpecified = matcher.group(3); final String showSpecified = matcher.group(2); return new myRunnable() { @Override public void run() { doShowSearch(episodeSpecified, showSpecified); } }; } p = Pattern.compile(getString(R.string.pattern_watch2)); matcher = p.matcher(queryText); if(matcher.find()) { final String queryTerm = matcher.group(2); logger.d("queryTerm: %s", queryTerm); return () -> doMovieSearch(queryTerm); } p = Pattern.compile(getString(R.string.pattern_listen_to_album_by_artist)); matcher = p.matcher(queryText); if(matcher.find()) { final String album = matcher.group(3); final String artist = matcher.group(4); return new myRunnable() { @Override public void run() { searchForAlbum(artist, album); } }; } p = Pattern.compile(getString(R.string.pattern_listen_to_album)); matcher = p.matcher(queryText); if(matcher.find()) { final String album = matcher.group(3); return new myRunnable() { @Override public void run() { searchForAlbum("", album); } }; } p = Pattern.compile(getString(R.string.pattern_listen_to_song_by_artist)); matcher = p.matcher(queryText); if(matcher.find()) { final String track = matcher.group(2); final String artist = matcher.group(3); return new myRunnable() { @Override public void run() { searchForSong(artist, track); } }; } p = Pattern.compile(getString(R.string.pattern_listen_to_artist)); matcher = p.matcher(queryText); if(matcher.find()) { final String artist = matcher.group(1); shuffle = true; // when specifying just an artist, shuffle all that artist's songs return new myRunnable() { @Override public void run() { searchForArtist(artist); } }; } p = Pattern.compile(getString(R.string.pattern_pause_playback), Pattern.DOTALL); matcher = p.matcher(queryText); if (matcher.find()) { return new StopRunnable() { @Override public void run() { pausePlayback(); } }; } p = Pattern.compile(getString(R.string.pattern_resume_playback), Pattern.DOTALL); matcher = p.matcher(queryText); if (matcher.find()) { logger.d("resuming playback"); return new StopRunnable() { @Override public void run() { resumePlayback(); } }; } p = Pattern.compile(getString(R.string.pattern_stop_playback), Pattern.DOTALL); matcher = p.matcher(queryText); if (matcher.find()) { logger.d("stopping playback"); return new StopRunnable() { @Override public void run() { stopPlayback(); } }; } p = Pattern.compile(getString(R.string.pattern_offset), Pattern.DOTALL); matcher = p.matcher(queryText); if (matcher.find()) { String groupOne = matcher.group(2) != null && matcher.group(2).matches("two|to") ? "2" : matcher.group(2); String groupThree = matcher.group(4) != null && matcher.group(4).matches("two|to") ? "2" : matcher.group(4); String groupFive = matcher.group(6) != null && matcher.group(6).matches("two|to") ? "2" : matcher.group(6); int hours = 0, minutes = 0, seconds = 0; if(matcher.group(5) != null && matcher.group(5).matches(getString(R.string.pattern_minutes))) minutes = Integer.parseInt(groupThree); else if(matcher.group(3) != null && matcher.group(3).matches(getString(R.string.pattern_minutes))) minutes = Integer.parseInt(groupOne); if(matcher.group(7) != null && matcher.group(7).matches(getString(R.string.pattern_seconds))) seconds = Integer.parseInt(groupFive); else if(matcher.group(5) != null && matcher.group(5).matches(getString(R.string.pattern_seconds))) seconds = Integer.parseInt(groupThree); else if(matcher.group(3).matches(getString(R.string.pattern_seconds))) seconds = Integer.parseInt(groupOne); if(matcher.group(3).matches(getString(R.string.pattern_hours))) hours = Integer.parseInt(groupOne); final int h = hours; final int m = minutes; final int s = seconds; return new StopRunnable() { @Override public void run() { seekTo(h, m, s); } }; } p = Pattern.compile(getString(R.string.pattern_forward), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { final String amount = matcher.group(1) != null && matcher.group(1).matches("two|to") ? "2" : matcher.group(1); final String del = matcher.group(2); logger.d("[ffr] del = %s", del); int mul = 1; // default multiplier, for seconds if(del.matches("minutes?")) mul = 60; else if(del.matches("hours?")) mul = 60*60; logger.d("[ffr] mul = %d", mul); final int seconds = Integer.parseInt(amount) * mul; return new StopRunnable() { @Override public void run() { logger.d("[ffr] Skipping ahead %d seconds", seconds); int currentOffset = subscriptionService.getPosition(); subscriptionService.seekTo(currentOffset + seconds); /* if(client.isCastClient) { int currentOffset = Integer.parseInt(subscriptionService.getNowPlayingMedia().viewOffset); logger.d("[ffr] currentOffset: %d", currentOffset); seekTo(currentOffset + (seconds * 1000)); } else { PlexHttpClient.getClientTimeline(client, 0, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { List<Timeline> timelines = mediaContainer.timelines; int currentTime = -1; if(timelines != null) { for (Timeline timeline : timelines) { if(!PlayerState.getState(timeline).equals(PlayerState.STOPPED)) currentTime = timeline.time; } } if(currentTime > -1) { logger.d("[ffr] currentOffset: %d", currentTime); seekTo(currentTime + (seconds * 1000)); } else { // TODO: Handle failure } } @Override public void onFailure(Throwable error) { logger.d("Failure getting client timeline"); error.printStackTrace(); } }); } */ } }; } p = Pattern.compile(getString(R.string.pattern_rewind), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { final String amount = matcher.group(2) != null && matcher.group(2).matches("two|to") ? "2" : matcher.group(2); final String del = matcher.group(3); logger.d("[ffr] del = %s", del); int mul = 1; // default multiplier, for seconds if(del.matches("minutes?")) mul = 60; else if(del.matches("hours?")) mul = 60*60; final int seconds = Integer.parseInt(amount) * mul; return new StopRunnable() { @Override public void run() { logger.d("[ffr] Rewinding %d seconds", seconds); int currentOffset = subscriptionService.getPosition(); subscriptionService.seekTo(currentOffset - seconds); /* if(client.isCastClient) { currentOffset = Integer.parseInt(subscriptionService.getNowPlayingMedia().viewOffset); logger.d("[ffr] currentOffset: %d", currentOffset); seekTo(currentOffset - (seconds * 1000)); } else { PlexHttpClient.getClientTimeline(client, 0, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { List<Timeline> timelines = mediaContainer.timelines; int currentTime = -1; if(timelines != null) { for (Timeline timeline : timelines) { if(!PlayerState.getState(timeline).equals(PlayerState.STOPPED)) currentTime = timeline.time; } } if(currentTime > -1) { logger.d("[ffr] currentOffset: %d", currentTime); seekTo(currentTime - (seconds * 1000)); } else { // TODO: Handle failure } } @Override public void onFailure(Throwable error) { logger.d("Failure getting client timeline"); error.printStackTrace(); } }); } */ } }; } p = Pattern.compile(getString(R.string.pattern_connect_to), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { final String connectToClient = matcher.group(1); PlexClient foundClient = null; for(PlexClient theClient : VoiceControlForPlexApplication.clients.values()) { if(compareTitle(theClient.name, connectToClient)) { foundClient = theClient; break; } } final PlexClient theClient = foundClient; if(foundClient == null) { if(didClientScan) { return new StopRunnable() { @Override public void run() { feedback.e(R.string.client_not_found); } }; } else { return new StopRunnable() { @Override public void run() { queries.add(0, queryText); sendClientScanIntent(); } }; } } else { return new StopRunnable() { @Override public void run() { logger.d("Service Subscribing to %s", theClient.name); subscriptionService.subscribe(theClient, true); } }; } } p = Pattern.compile(getString(R.string.pattern_disconnect), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { subscriptionService.unsubscribe(); } }; } p = Pattern.compile(getString(R.string.pattern_cycle_subtitles), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { subscriptionService.cycleStreams(Stream.SUBTITLE); } }; } p = Pattern.compile(getString(R.string.pattern_cycle_audio), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { subscriptionService.cycleStreams(Stream.AUDIO); } }; } p = Pattern.compile(getString(R.string.pattern_subtitles_off), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { subscriptionService.subtitlesOff(); } }; } p = Pattern.compile(getString(R.string.pattern_subtitles_on), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { subscriptionService.subtitlesOn(); } }; } p = Pattern.compile(getString(R.string.pattern_whats_new_movies), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { whatsNewMovies(); } }; } p = Pattern.compile(getString(R.string.pattern_whats_new), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { whatsNew(); } }; } p = Pattern.compile(getString(R.string.pattern_whats_on_deck), Pattern.DOTALL); matcher = p.matcher(queryText); if(matcher.find()) { return new StopRunnable() { @Override public void run() { whatsOnDeck(); } }; } if(queries.size() > 0) return null; else { return new myRunnable() { @Override public void run() { feedback.e(getString(R.string.didnt_understand), queryText); } }; } } private void sendCommandToLocalPlayer(String which) { logger.d("Sending command to local player: %s", whichLocalPlayer); Intent nowPlayingIntent = new Intent(this, whichLocalPlayer == com.atomjack.shared.Intent.PLAYER_AUDIO ? MainActivity.class : VideoPlayerActivity.class); nowPlayingIntent.setAction(com.atomjack.shared.Intent.ACTION_MIC_RESPONSE); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.ACTION_VIDEO_COMMAND, which); // 'which' is one of Intent.ACTION_* nowPlayingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(nowPlayingIntent); } private void adjustPlayback(String which, final String onFinish) { ArrayList<String> validModes = new ArrayList<>(Arrays.asList(com.atomjack.shared.Intent.ACTION_PAUSE, com.atomjack.shared.Intent.ACTION_PLAY, com.atomjack.shared.Intent.ACTION_STOP)); if(validModes.indexOf(which) == -1) return; if(client.isLocalClient) { //whichLocalPlayer sendCommandToLocalPlayer(which); return; } else { if(which.equals(com.atomjack.shared.Intent.ACTION_PAUSE)) subscriptionService.pause(); else if(which.equals(com.atomjack.shared.Intent.ACTION_PLAY)) subscriptionService.play(); else if(which.equals(com.atomjack.shared.Intent.ACTION_STOP)) subscriptionService.stop(); return; } /* } else if(client.isCastClient) { if(which.equals(com.atomjack.shared.Intent.ACTION_PAUSE)) subscriptionService.pause(); else if(which.equals(com.atomjack.shared.Intent.ACTION_PLAY)) subscriptionService.play(); else if(which.equals(com.atomjack.shared.Intent.ACTION_STOP)) castPlayerManager.stop(); return; } PlexHttpResponseHandler responseHandler = new PlexHttpResponseHandler() { @Override public void onSuccess(PlexResponse r) { Boolean passed = true; if(r.code != 200) { passed = false; } logger.d("Playback response: %d", r.code); if(passed) { feedback.m(onFinish); } else { feedback.e(getResources().getString(R.string.http_status_code_error), r.code); } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }; if(which.equals(com.atomjack.shared.Intent.ACTION_PAUSE)) client.pause(responseHandler); else if(which.equals(com.atomjack.shared.Intent.ACTION_PLAY)) client.play(responseHandler); else if(which.equals(com.atomjack.shared.Intent.ACTION_STOP)) client.stop(responseHandler); */ } private void pausePlayback() { adjustPlayback(com.atomjack.shared.Intent.ACTION_PAUSE, getResources().getString(R.string.playback_paused)); } private void resumePlayback() { adjustPlayback(com.atomjack.shared.Intent.ACTION_PLAY, getResources().getString(R.string.playback_resumed)); } private void stopPlayback() { adjustPlayback(com.atomjack.shared.Intent.ACTION_STOP, getResources().getString(R.string.playback_stopped)); } private void whatsNewMovies() { serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.movieSectionsSearched = 0; if(server.movieSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { whatsNewMoviesFinished(); } } else { for(int i=0;i<server.movieSections.size();i++) { PlexHttpClient.getRecentlyAdded(server, server.movieSections.get(i), new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { server.movieSectionsSearched++; videos.addAll(mediaContainer.videos); if (server.movieSections.size() == server.movieSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { whatsNewMoviesFinished(); } } } @Override public void onFailure(Throwable error) { server.movieSectionsSearched++; if (server.movieSections.size() == server.movieSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { whatsNewMoviesFinished(); } } } }); } } } } private void whatsNewMoviesFinished() { if(videos.size() == 0) { feedback.v(R.string.no_new_movies); } else { videos = videos.subList(0, 5); List<String> titlesArr = new ArrayList<>(); for (PlexVideo video : videos) titlesArr.add(video.getTitle()); String titles = Utils.implode(", ", ", and ", titlesArr.toArray(new String[titlesArr.size()])); feedback.v(String.format(getString(R.string.whats_new_movies_return), titles)); } } private void whatsNew() { serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.tvSectionsSearched = 0; if(server.tvSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { whatsNewFinished(); } } else { for(int i=0;i<server.tvSections.size();i++) { PlexHttpClient.getRecentlyAdded(server, server.tvSections.get(i), new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { server.tvSectionsSearched++; videos.addAll(mediaContainer.videos); if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { whatsNewFinished(); } } } @Override public void onFailure(Throwable error) { server.tvSectionsSearched++; if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { whatsNewFinished(); } } } }); } } } } private void whatsNewFinished() { if(videos.size() == 0) { feedback.v(R.string.nothing_new); } else { videos = videos.subList(0, 5); List<String> titlesArr = new ArrayList<>(); for (PlexVideo video : videos) titlesArr.add(video.getTitle()); String titles = Utils.implode(", ", ", and ", titlesArr.toArray(new String[titlesArr.size()])); feedback.v(String.format(getString(R.string.whats_new_return), titles)); } } private void whatsOnDeck() { serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.tvSectionsSearched = 0; if(server.tvSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { onDeckFinished(); } } else { for(int i=0;i<server.tvSections.size();i++) { PlexHttpClient.getOnDeck(server, server.tvSections.get(i), new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { server.tvSectionsSearched++; videos.addAll(mediaContainer.videos); if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { onDeckFinished(); } } } @Override public void onFailure(Throwable error) { server.tvSectionsSearched++; if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { onDeckFinished(); } } } }); } } } } private void onDeckFinished() { if(videos.size() == 0) { feedback.v(R.string.nothing_on_deck); } else { videos = videos.subList(0, 5); List<String> titlesArr = new ArrayList<>(); for (PlexVideo video : videos) titlesArr.add(video.getTitle()); String titles = Utils.implode(", ", ", and ", titlesArr.toArray(new String[titlesArr.size()])); feedback.v(String.format(getString(R.string.on_deck_return), titles)); } } private void seekTo(int hours, int minutes, int seconds) { logger.d("Seeking to %d hours, %d minutes, %d seconds", hours, minutes, seconds); int offset = 1000*((hours*60*60)+(minutes*60)+seconds); logger.d("offset: %d milliseconds", offset); seekTo(offset); } private void seekTo(int offset) { logger.d("Seeking to %d", offset); subscriptionService.seekTo(offset / 1000); /* if(client.isCastClient) { castPlayerManager.seekTo(offset / 1000); } else { client.seekTo(offset, plexSubscription.getNowPlayingMedia().isMusic() ? "music" : "video", new PlexHttpResponseHandler() { @Override public void onSuccess(PlexResponse r) { Boolean passed = true; if (r.code != 200) { passed = false; } logger.d("Playback response: %d", r.code); if (!passed) { feedback.e(getResources().getString(R.string.http_status_code_error), r.code); } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } */ } private void videoAttemptedOnAudioOnlyDevice() { feedback.e(String.format(getString(R.string.video_attempted_on_audio_only_device), client.name)); } private void doMovieSearch(final String queryTerm) { logger.d("Doing movie search. %d servers", plexmediaServers.size()); if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } feedback.m(getString(R.string.searching_for), queryTerm); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.movieSectionsSearched = 0; logger.d("Searching server (for movies): %s, %d sections", server.name, server.movieSections.size()); if(server.movieSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { onMovieSearchFinished(queryTerm); } } for(int i=0;i<server.movieSections.size();i++) { String section = server.movieSections.get(i); // String path = String.format("/library/sections/%s/search?type=1&query=%s", section, queryTerm.replace("&", "%26").replaceAll(" ", "%20")); PlexHttpClient.searchServer(server, section, queryTerm, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.movieSectionsSearched++; for (int j = 0; j < mc.videos.size(); j++) { PlexVideo video = mc.videos.get(j); if (compareTitle(video.title.toLowerCase(), queryTerm.toLowerCase())) { video.server = server; video.showTitle = mc.grandparentTitle; video.parentArt = mc.art; videos.add(video); } } logger.d("Videos: %d", mc.videos.size()); logger.d("%d sections searched out of %d", server.movieSectionsSearched, server.movieSections.size()); if (server.movieSections.size() == server.movieSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { onMovieSearchFinished(queryTerm); } } } @Override public void onFailure(Throwable error) { error.printStackTrace(); feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if(serversSearched == plexmediaServers.size()) { onMovieSearchFinished(queryTerm); } } }); } } private static boolean compareTitle(String title, String queryTerm) { // Replace & in the title with "and" // title = title.replace("&", "and"); // First, check if the two terms are equal if(title.toLowerCase().equals(queryTerm.toLowerCase())) return true; // Strip out some other punctuation from the title, like periods and commas title = title.replaceAll("[\\.,]", ""); // Check for an exact match again if(title.toLowerCase().equals(queryTerm.toLowerCase())) return true; // No equal match, so split the query term up by words, and see if the title contains every single word String[] words = queryTerm.split(" "); boolean missing = false; for(int i=0;i<words.length;i++) { if(!title.toLowerCase().matches(".*\\b" + words[i].toLowerCase() + "\\b.*")) missing = true; } return !missing; } private void onMovieSearchFinished(String queryTerm) { logger.d("Done searching! Have movies: %d", videos.size()); if(videos.size() == 1) { logger.d("Chosen video: %s", videos.get(0).title); fetchAndPlayMedia(videos.get(0)); } else if(videos.size() > 1) { // We found more than one match, but let's see if any of them are an exact match Boolean exactMatch = false; for(int i=0;i<videos.size();i++) { logger.d("Looking at video %s", videos.get(i).title); if(videos.get(i).title.toLowerCase().equals(queryTerm.toLowerCase())) { logger.d("found exact match!"); exactMatch = true; fetchAndPlayMedia(videos.get(i)); break; } } if(!exactMatch) { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.found_more_than_one_movie)); return; } } else { logger.d("Didn't find a video"); // Let's also support using this syntax to play the next episode in a tv show. Probably will want to use a different error message if nothing is found, though. doNextEpisodeSearch(queryTerm, true); } } private void requestTransientAccessToken(PlexServer server, final AfterTransientTokenRequest onFinish) { String path = "/security/token?type=delegation&scope=all"; PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { onFinish.success(mediaContainer.token); } @Override public void onFailure(Throwable error) { onFinish.failure(); } }); } // When searching for media, the media element contained inside the media container doesn't necessarily have a complete set of attributes. So, // fetch the specific media element by its key. This prevents the need to add new missing fields to the media // private void fetchAndPlayMedia(final PlexMedia media) { logger.d("fetchAndPlayMedia: %s (%s)", media.title, media.key); PlexHttpClient.get(media.server, media.key, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { PlexMedia theMedia = null; if (media instanceof PlexVideo) theMedia = mediaContainer.videos.get(0); else if (media instanceof PlexTrack) theMedia = mediaContainer.tracks.get(0); if (theMedia != null) { theMedia.server = media.server; logger.d("fetchAndPlayMedia, set server to %s", theMedia.server.name); playMedia(theMedia); onActionFinished(WearConstants.SPEECH_QUERY_RESULT, false, theMedia); } else { // TODO: Handle failure logger.d("Failed!"); } } @Override public void onFailure(Throwable error) { // TODO: Handle failure error.printStackTrace(); } }); } private void playMedia(final PlexMedia media) { playMedia(media, null); } private void playMedia(final PlexMedia media, final PlexDirectory album) { // TODO: switch this to the PlexServer method and verify requestTransientAccessToken(media.server, new AfterTransientTokenRequest() { @Override public void success(String token) { createPlayQueueAndPlayMedia(media, album, token); } @Override public void failure() { // Just try to play without a transient token createPlayQueueAndPlayMedia(media, album, null); } }); } private void playAllFromArtist(final PlexDirectory artist) { logger.d("Playing all tracks from %s", artist.title); artist.server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(final Connection connection) { PlexHttpClient.createArtistPlayQueue(connection, artist, new PlexPlayQueueHandler() { @Override public void onSuccess(final MediaContainer mediaContainer) { logger.d("got play queue: %s", mediaContainer.playQueueID); tracks = mediaContainer.tracks; if (tracks.size() > 0) { for(PlexTrack track : mediaContainer.tracks) { track.server = artist.server; } final PlexTrack media = tracks.get(0); requestTransientAccessToken(media.server, new AfterTransientTokenRequest() { @Override public void success(String token) { playMedia(media, connection, null, token, mediaContainer); } @Override public void failure() { playMedia(media, connection, null, null, mediaContainer); } }); } } }); } @Override public void onFailure(int statusCode) { // TODO: Handle failure } }); } private int getOffset(PlexMedia media) { logger.d("getting offset, mediaoffset: %s", media.viewOffset); if((VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false) || resumePlayback) && media.viewOffset != null) return Integer.parseInt(media.viewOffset) / 1000; else return 0; } private void playLocalMedia(PlexMedia media, String transientToken, MediaContainer mediaContainer) { subscriptionService.subscribe(PlexClient.getLocalPlaybackClient(), false); final Intent nowPlayingIntent = new Intent(this, media instanceof PlexVideo ? VideoPlayerActivity.class : MainActivity.class); nowPlayingIntent.setAction(com.atomjack.shared.Intent.ACTION_PLAY_LOCAL); nowPlayingIntent.putExtra(WearConstants.FROM_WEAR, fromWear); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_STARTING_PLAYBACK, true); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_MEDIA, media); if(mediaContainer != null) { nowPlayingIntent.putParcelableArrayListExtra(com.atomjack.shared.Intent.EXTRA_PLAYLIST, media.isMusic() ? mediaContainer.tracks : mediaContainer.videos); } nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_TRANSIENT_TOKEN, transientToken); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_RESUME, resumePlayback); nowPlayingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(nowPlayingIntent); } private void preCachePlayerImages(PlexMedia firstMedia, ArrayList<? extends PlexMedia> playlist, final Runnable onFinish) { if(playlist == null) { onFinish.run(); return; } final int[] numMedia = new int[]{0}; // the total number final int[] mediaDone = new int[]{0}; String[] posterPrefs = VoiceControlForPlexApplication.getMediaPosterPrefs(firstMedia); final int posterWidth = VoiceControlForPlexApplication.getInstance().prefs.get(posterPrefs[0], -1); final int posterHeight = VoiceControlForPlexApplication.getInstance().prefs.get(posterPrefs[1], -1); // Since we can't reliably get the dimensions for the poster from here, they will be saved the first time this type of // player is launched (video or music). Then prefetching of posters for that type and orientation will happen. if(posterWidth == -1 || posterHeight == -1) { if(onFinish != null) onFinish.run(); return; } final PlexMedia.IMAGE_KEY notificationImageKey = firstMedia.isMusic() ? PlexMedia.IMAGE_KEY.NOTIFICATION_THUMB_MUSIC : PlexMedia.IMAGE_KEY.NOTIFICATION_THUMB; final PlexMedia.IMAGE_KEY notificationImageKeyBig = firstMedia.isMusic() ? PlexMedia.IMAGE_KEY.NOTIFICATION_THUMB_MUSIC_BIG : PlexMedia.IMAGE_KEY.NOTIFICATION_THUMB_BIG; final PlexMedia.IMAGE_KEY mainImageKey; if(firstMedia.isMusic()) mainImageKey = PlexMedia.IMAGE_KEY.MUSIC_THUMB; else if(firstMedia.isShow()) mainImageKey = PlexMedia.IMAGE_KEY.SHOW_THUMB; else mainImageKey = PlexMedia.IMAGE_KEY.MOVIE_THUMB; // compile list of albums/videos in the rest of the playlist (excluding first one, which is fetched below) that we should fetch images for final ArrayList<PlexMedia> list = new ArrayList<>(); List<String> keysToFetch = new ArrayList<>(); for(final PlexMedia m : playlist) { if(m.isMusic()) { if (!keysToFetch.contains(((PlexTrack)m).parentRatingKey) && !firstMedia.ratingKey.equals(m.ratingKey)) { if(VoiceControlForPlexApplication.getInstance().getCachedBitmap(m.getImageKey(notificationImageKey)) == null || VoiceControlForPlexApplication.getInstance().getCachedBitmap(m.getImageKey(notificationImageKeyBig)) == null || VoiceControlForPlexApplication.getInstance().getCachedBitmap(m.getImageKey(mainImageKey)) == null) { keysToFetch.add(((PlexTrack) m).parentRatingKey); list.add(m); } } } else { if(!keysToFetch.contains(m.ratingKey) && !firstMedia.ratingKey.equals(m.ratingKey)) { if(VoiceControlForPlexApplication.getInstance().getCachedBitmap(m.getImageKey(notificationImageKey)) == null || VoiceControlForPlexApplication.getInstance().getCachedBitmap(m.getImageKey(notificationImageKeyBig)) == null || VoiceControlForPlexApplication.getInstance().getCachedBitmap(m.getImageKey(mainImageKey)) == null) { keysToFetch.add(m.ratingKey); list.add(m); } } } } logger.d("After fetching images for first media, we will fetch %d more images", list.size()*3); BitmapHandler bitmapHandler = new BitmapHandler() { @Override public void onSuccess(Bitmap bitmap) { mediaDone[0]++; if (mediaDone[0] >= numMedia[0]) { if(onFinish != null) onFinish.run(); } else return; if(list.size() > 0) { LimitedAsyncTask newTaskList = new LimitedAsyncTask(); // Fetch the images for the rest of the tracks for (final PlexMedia m : list) { if (m.thumb != null || m.grandparentThumb != null) { String mainThumb; if (m.isMusic()) mainThumb = m.thumb != null ? m.thumb : m.grandparentThumb; else mainThumb = m.isShow() ? m.thumb : m.grandparentThumb; newTaskList.addTask(new FetchMediaImageTask(m, posterWidth, posterHeight, mainThumb, m.getImageKey(mainImageKey))); newTaskList.addTask(new FetchMediaImageTask(m, PlexMedia.IMAGE_SIZES.get(notificationImageKey)[0], PlexMedia.IMAGE_SIZES.get(notificationImageKey)[1], m.getNotificationThumb(notificationImageKey), m.getImageKey(notificationImageKey))); newTaskList.addTask(new FetchMediaImageTask(m, PlexMedia.IMAGE_SIZES.get(notificationImageKeyBig)[0], PlexMedia.IMAGE_SIZES.get(notificationImageKeyBig)[1], m.getNotificationThumb(notificationImageKeyBig), m.getImageKey(notificationImageKeyBig))); } } newTaskList.run(); } } }; // fetch image for main and notification images if(firstMedia.thumb != null || firstMedia.grandparentThumb != null) { String mainThumb; if(firstMedia.isMusic()) mainThumb = firstMedia.thumb != null ? firstMedia.thumb : firstMedia.grandparentThumb; else mainThumb = !firstMedia.isShow() ? firstMedia.thumb : firstMedia.grandparentThumb; new FetchMediaImageTask(firstMedia, posterWidth, posterHeight, mainThumb, firstMedia.getImageKey(mainImageKey), bitmapHandler).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new FetchMediaImageTask(firstMedia, PlexMedia.IMAGE_SIZES.get(notificationImageKey)[0], PlexMedia.IMAGE_SIZES.get(notificationImageKey)[1], firstMedia.getNotificationThumb(notificationImageKey), firstMedia.getImageKey(notificationImageKey), bitmapHandler).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new FetchMediaImageTask(firstMedia, PlexMedia.IMAGE_SIZES.get(notificationImageKeyBig)[0], PlexMedia.IMAGE_SIZES.get(notificationImageKeyBig)[1], firstMedia.getNotificationThumb(notificationImageKeyBig), firstMedia.getImageKey(notificationImageKeyBig), bitmapHandler).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); numMedia[0] += 3; } else { bitmapHandler.onSuccess(null); } } private void playMedia(final PlexMedia media, Connection connection, PlexDirectory album, final String transientToken, final MediaContainer mediaContainer) { if(client.isLocalClient) { if(mediaContainer != null) { final int[] numMedia = new int[]{0}; // the total number final int[] mediaDone = new int[]{0}; if(media.isMusic()) { preCachePlayerImages(media, mediaContainer.tracks, new Runnable() { @Override public void run() { playLocalMedia(media, transientToken, mediaContainer); } }); } else { // Fetch the video background and thumbnail for the first video in the playlist. Once that is done, launch the video player, and also fetch // the background and thumbnail for the remaining videos in the playlist (if there are any). If the first video has no background OR thumbnail, // immediately launch the player (and fetch the remaining videos' images) int[] dims = VoiceControlForPlexApplication.getScreenDimensions(PlexSearchService.this); final int width = dims[0]; final int height = dims[1]; final PlexVideo firstVideo = mediaContainer.videos.get(0); BitmapHandler onFinish = new BitmapHandler() { @Override public void onSuccess(Bitmap b) { mediaDone[0]++; if (mediaDone[0] == numMedia[0]) { playLocalMedia(media, transientToken, mediaContainer); // Fetch art and thumbnail for the rest of the videos in the container List<FetchMediaImageTask> taskList = new ArrayList<>(); for(final PlexVideo m : mediaContainer.videos) { if(!m.key.equals(firstVideo.key)) { String background = m.isShow() ? m.grandparentArt : m.art; String thumb = m.isShow() ? m.grandparentThumb : m.thumb; if (background != null) { taskList.add(new FetchMediaImageTask(m, width, height, background, m.getImageKey(PlexMedia.IMAGE_KEY.LOCAL_VIDEO_BACKGROUND))); } if (thumb != null) { taskList.add(new FetchMediaImageTask(m, com.google.android.libraries.cast.companionlibrary.utils.Utils.convertDpToPixel(PlexSearchService.this, getResources().getDimension(R.dimen.video_player_poster_width)), com.google.android.libraries.cast.companionlibrary.utils.Utils.convertDpToPixel(PlexSearchService.this, getResources().getDimension(R.dimen.video_player_poster_height)), thumb, m.getImageKey(PlexMedia.IMAGE_KEY.LOCAL_VIDEO_THUMB))); } } } for (FetchMediaImageTask task : taskList) task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } }; logger.d("%s is show: %s", firstVideo.getTitle(), firstVideo.isShow()); String background = firstVideo.isShow() ? firstVideo.grandparentArt : firstVideo.art; String thumb = firstVideo.isShow() ? firstVideo.grandparentThumb : firstVideo.thumb; if(background != null) { numMedia[0]++; new FetchMediaImageTask(firstVideo, width, height, background, firstVideo.getImageKey(PlexMedia.IMAGE_KEY.LOCAL_VIDEO_BACKGROUND), onFinish).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } if(thumb != null) { numMedia[0]++; new FetchMediaImageTask(firstVideo, com.google.android.libraries.cast.companionlibrary.utils.Utils.convertDpToPixel(this, getResources().getDimension(R.dimen.video_player_poster_width)), com.google.android.libraries.cast.companionlibrary.utils.Utils.convertDpToPixel(this, getResources().getDimension(R.dimen.video_player_poster_height)), thumb, firstVideo.getImageKey(PlexMedia.IMAGE_KEY.LOCAL_VIDEO_THUMB), onFinish).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } // Just in case the first video in the playlist doesn't have either art or thumb, force play if(firstVideo.art == null && firstVideo.thumb == null) { mediaDone[0] = -1; // Need to do this in order to ensure that the video player is launched, since we're expecting 0 videos onFinish.onSuccess(null); } } } else if(album != null) { PlexHttpClient.getChildren(album, media.server, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mediaContainer) { // Add the server to each track for(PlexTrack t : mediaContainer.tracks) t.server = media.server; for(PlexVideo v : mediaContainer.videos) v.server = media.server; playLocalMedia(media, transientToken, mediaContainer); } @Override public void onFailure(Throwable error) { } }); } else playLocalMedia(media, transientToken, mediaContainer); } else if(client.isCastClient) { logger.d("num videos/tracks: %d", media instanceof PlexTrack ? mediaContainer.tracks.size() : mediaContainer.videos.size()); Runnable sendCast = () -> { subscriptionService.loadMedia(media instanceof PlexTrack ? mediaContainer.tracks.get(0) : mediaContainer.videos.get(0), media instanceof PlexTrack ? (ArrayList)mediaContainer.tracks : (ArrayList)mediaContainer.videos, getOffset(media instanceof PlexTrack ? mediaContainer.tracks.get(0) : mediaContainer.videos.get(0))); if(showPlayer) showPlayingMedia(media, mediaContainer); }; if(subscriptionService.isSubscribed()) { sendCast.run(); } else { subscriptionService.subscribeToChromecast(client, sendCast, true); } } else { QueryString qs = VoiceControlForPlexApplication.getPlaybackQueryString(media, mediaContainer, connection, transientToken, album, resumePlayback); logger.d("Resume playback: %s, qs: %s", resumePlayback, qs); PlexHttpClient.get(String.format("http://%s:%s", client.address, client.port), String.format("player/playback/playMedia?%s", qs), new PlexHttpResponseHandler() { @Override public void onSuccess(PlexResponse r) { // If the host we're playing on is this device, we don't wanna do anything else here. if (Utils.getIPAddress(true).equals(client.address) || r == null) return; if (media instanceof PlexTrack) feedback.m(getResources().getString(R.string.now_listening_to), media.title, ((PlexTrack) media).getArtist(), client.name); else feedback.m(getResources().getString(R.string.now_watching_video), media.isMovie() ? media.title : media.grandparentTitle, client.name); boolean passed = true; if (r.code != 200) { passed = false; } logger.d("Playback response: %s", r.code); if (passed) { videoPlayed = true; if(showPlayer) showPlayingMedia(media.isMusic() ? mediaContainer.tracks.get(0) : mediaContainer.videos.get(0), mediaContainer); } else { feedback.e(getResources().getString(R.string.http_status_code_error), r.code); } } @Override public void onFailure(Throwable error) { feedback.e(String.format(getResources().getString(R.string.couldnt_play_to_client), client.name)); logger.e("Couldn't connect to client %s.", client.name); error.printStackTrace(); } }); } } private void createPlayQueueAndPlayMedia(final PlexMedia media, final PlexDirectory album, final String transientToken) { logger.d("Playing media: %s", media.title); logger.d("Client: %s", client); logger.d("currentNetworkState: %s", currentNetworkState); if(currentNetworkState == MainActivity.NetworkState.MOBILE && !client.isLocalClient) { media.server.localPlay(media, resumePlayback, transientToken); } else if(currentNetworkState == MainActivity.NetworkState.WIFI || client.isLocalClient) { media.server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(final Connection connection) { try { PlexHttpClient.createPlayQueue(connection, media, resumePlayback, album != null ? album.ratingKey : media.ratingKey, transientToken, mediaContainer -> { logger.d("Play queue id: %s", mediaContainer.playQueueID); playMedia(media, connection, album, transientToken, mediaContainer); }); } catch (Exception e) { feedback.e(getResources().getString(R.string.got_error), e.getMessage()); logger.e("Exception trying to play video: %s", e.toString()); e.printStackTrace(); } } @Override public void onFailure(int statusCode) { // TODO: Handle no connection? } }); } } private void showPlayingMedia(final PlexMedia media, final MediaContainer mediaContainer) { logger.d("nowPlayingMedia: %s", media.title); preCachePlayerImages(media, mediaContainer.tracks, () -> { Intent nowPlayingIntent = new Intent(PlexSearchService.this, MainActivity.class); nowPlayingIntent.setAction(MainActivity.ACTION_SHOW_NOW_PLAYING); nowPlayingIntent.putExtra(WearConstants.FROM_WEAR, fromWear); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_STARTING_PLAYBACK, true); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_MEDIA, media); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_CLIENT, client); nowPlayingIntent.putParcelableArrayListExtra(com.atomjack.shared.Intent.EXTRA_PLAYLIST, media.isMusic() ? mediaContainer.tracks : mediaContainer.videos); nowPlayingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(nowPlayingIntent); }); } private void doNextEpisodeSearch(final String queryTerm, final boolean fallback) { if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } feedback.m(getString(R.string.searching_for), queryTerm); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { logger.d("Searching (for next episode) %s for %s", server.name, queryTerm); server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.tvSectionsSearched = 0; if (server.tvSections.size() == 0) { serversSearched++; if (serversSearched == plexmediaServers.size()) { onFinishedNextEpisodeSearch(queryTerm, fallback); } } for (int i = 0; i < server.tvSections.size(); i++) { String section = server.tvSections.get(i); String path = String.format("/library/sections/%s/onDeck", section); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.tvSectionsSearched++; for (int j = 0; j < mc.videos.size(); j++) { PlexVideo video = mc.videos.get(j); if (compareTitle(video.grandparentTitle, queryTerm)) { video.server = server; video.thumb = video.grandparentThumb; video.showTitle = video.grandparentTitle; video.parentArt = mc.art; videos.add(video); logger.d("ADDING " + video.grandparentTitle); } } if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { onFinishedNextEpisodeSearch(queryTerm, fallback); } } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if (serversSearched == plexmediaServers.size()) { onFinishedNextEpisodeSearch(queryTerm, fallback); } } }); } } private void onFinishedNextEpisodeSearch(String queryTerm, boolean fallback) { if(videos.size() == 0) { if(queries.size() == 0) feedback.e(getResources().getString(fallback ? R.string.couldnt_find : R.string.couldnt_find_next), queryTerm); else { startup(); } } else { if(videos.size() == 1) fetchAndPlayMedia(videos.get(0)); else { // We found more than one matching show. Let's check if the title of any of the matching shows // exactly equals the query term, otherwise tell the user to be more specific. // int exactMatch = -1; for(int i=0;i<videos.size();i++) { if(videos.get(i).grandparentTitle.toLowerCase().equals(queryTerm.toLowerCase())) { exactMatch = i; break; } } if(exactMatch > -1) { fetchAndPlayMedia(videos.get(exactMatch)); } else { feedback.e(getResources().getString(R.string.found_more_than_one_show)); return; } } } } private void doLatestEpisodeSearch(final String queryTerm) { if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } feedback.m(getString(R.string.searching_for), queryTerm); logger.d("doLatestEpisodeSearch: %s", queryTerm); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.tvSectionsSearched = 0; logger.d("Searching server %s", server.name); if (server.tvSections.size() == 0) { logger.d(server.name + " has no tv sections"); serversSearched++; if (serversSearched == plexmediaServers.size()) { doLatestEpisode(queryTerm); } } for (int i = 0; i < server.tvSections.size(); i++) { String section = server.tvSections.get(i); String path = String.format("/library/sections/%s/search?type=2&query=%s", section, queryTerm); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.tvSectionsSearched++; for (int j = 0; j < mc.directories.size(); j++) { PlexDirectory show = mc.directories.get(j); if (compareTitle(show.title, queryTerm)) { show.server = server; shows.add(show); logger.d("Adding %s", show.title); } } if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { doLatestEpisode(queryTerm); } } } @Override public void onFailure(Throwable error) { error.printStackTrace(); feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if (serversSearched == plexmediaServers.size()) { doLatestEpisode(queryTerm); } } }); } } private void doLatestEpisode(final String queryTerm) { if(shows.size() == 0) { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find), queryTerm); return; } PlexDirectory chosenShow = null; if(shows.size() > 1) { for(int i=0;i<shows.size();i++) { PlexDirectory show = shows.get(i); if(show.title.toLowerCase().equals(queryTerm.toLowerCase())) { chosenShow = show; break; } } } else { chosenShow = shows.get(0); } if(chosenShow == null) { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.found_more_than_one_show)); return; } final PlexDirectory show = chosenShow; String path = String.format("/library/metadata/%s/allLeaves", show.ratingKey); PlexHttpClient.get(show.server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { PlexVideo latestVideo = null; for(int j=0;j<mc.videos.size();j++) { PlexVideo video = mc.videos.get(j); if(latestVideo == null || (video.airDate() != null && latestVideo.airDate().before(video.airDate()))) { // video.showTitle = video.grandparentTitle; // video.parentArt = mc.art; // video.grandparentThumb = mc.art.replaceAll("\\/art\\/", "\\/thumb\\/"); latestVideo = video; } } latestVideo.server = show.server; logger.d("Found video: %s", latestVideo.airDate()); if(latestVideo != null) { fetchAndPlayMedia(latestVideo); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find), queryTerm); return; } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } private void doShowSearch(final String episodeSpecified, final String showSpecified) { if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } feedback.m(getString(R.string.searching_for_episode), showSpecified, episodeSpecified); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.tvSectionsSearched = 0; if (server.tvSections.size() == 0) { serversSearched++; if (serversSearched == plexmediaServers.size()) { playSpecificEpisode(showSpecified); } } for (int i = 0; i < server.tvSections.size(); i++) { String section = server.tvSections.get(i); String path = String.format("/library/sections/%s/search?type=4&query=%s", section, episodeSpecified); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.tvSectionsSearched++; for (int j = 0; j < mc.videos.size(); j++) { logger.d("Show: %s", mc.videos.get(j).grandparentTitle); PlexVideo video = mc.videos.get(j); if (compareTitle(video.grandparentTitle, showSpecified)) { video.server = server; video.thumb = video.grandparentThumb; video.showTitle = video.grandparentTitle; video.parentArt = mc.art; logger.d("Adding %s - %s.", video.showTitle, video.title); videos.add(video); } } if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { playSpecificEpisode(showSpecified); } } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if (serversSearched == plexmediaServers.size()) { playSpecificEpisode(showSpecified); } } }); } } private void playRandomEpisode(String showSpecified) { if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } feedback.m(getString(R.string.searching_for_random_episode), showSpecified); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.tvSectionsSearched = 0; if (server.tvSections.size() == 0) { serversSearched++; if (serversSearched == plexmediaServers.size()) { playSpecificEpisode(showSpecified); } } for (int i = 0; i < server.tvSections.size(); i++) { String section = server.tvSections.get(i); PlexHttpClient.getRandomEpisode(server, connection, showSpecified, section, media -> { server.tvSectionsSearched++; if(media != null) { PlexVideo video = (PlexVideo)media; video.server = server; video.thumb = video.grandparentThumb; video.showTitle = video.grandparentTitle; logger.d("Adding %s - %s.", video.showTitle, video.title); videos.add(video); } if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { playSpecificEpisode(showSpecified); } } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if (serversSearched == plexmediaServers.size()) { playSpecificEpisode(showSpecified); } } }); } } private void playSpecificEpisode(String showSpecified) { if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } if(videos.size() == 0) { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_episode)); } else if(videos.size() == 1) { fetchAndPlayMedia(videos.get(0)); } else { Boolean exactMatch = false; for(int i=0;i<videos.size();i++) { if(videos.get(i).grandparentTitle.toLowerCase().equals(showSpecified.toLowerCase())) { exactMatch = true; fetchAndPlayMedia(videos.get(i)); break; } } if(!exactMatch) { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.found_more_than_one_show)); } } } private void doShowSearch(final String queryTerm, final String season, final String episode) { if(client.isCastClient && client.isAudioOnly) { videoAttemptedOnAudioOnlyDevice(); return; } feedback.m(getString(R.string.searching_for_show_season_episode), queryTerm, season, episode); logger.d("doShowSearch: %s s%s e%s", queryTerm, season, episode); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.tvSectionsSearched = 0; logger.d("Searching server %s", server.name); if (server.tvSections.size() == 0) { logger.d("%s has no tv sections", server.name); serversSearched++; if (serversSearched == plexmediaServers.size()) { doEpisodeSearch(queryTerm, season, episode); } } for (int i = 0; i < server.tvSections.size(); i++) { String section = server.tvSections.get(i); String path = String.format("/library/sections/%s/search?type=2&query=%s", section, queryTerm); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.tvSectionsSearched++; for (int j = 0; j < mc.directories.size(); j++) { shows.add(mc.directories.get(j)); } if (server.tvSections.size() == server.tvSectionsSearched) { serversSearched++; if (serversSearched == plexmediaServers.size()) { doEpisodeSearch(queryTerm, season, episode); } } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if (serversSearched == plexmediaServers.size()) { doEpisodeSearch(queryTerm, season, episode); } } }); } } private void doEpisodeSearch(String queryTerm, final String season, final String episode) { logger.d("Found shows: %d", shows.size()); serversSearched = 0; for(final PlexServer server : plexmediaServers.values()) { if(shows.size() == 0 && serversSearched == plexmediaServers.size()) { serversSearched++; if(queries.size() == 0) feedback.e(getResources().getString(R.string.couldnt_find), queryTerm); else startup(); } else if(shows.size() == 1) { final PlexDirectory show = shows.get(0); logger.d("Show key: %s", show.key); PlexHttpClient.get(server, show.key, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { PlexDirectory foundSeason = null; for(int i=0;i<mc.directories.size();i++) { PlexDirectory directory = mc.directories.get(i); if(directory.title.equals("Season " + season)) { logger.d("Found season %s: %s.", season, directory.key); foundSeason = directory; break; } } if(foundSeason == null && serversSearched == plexmediaServers.size() && !videoPlayed) { serversSearched++; if(queries.size() == 0) feedback.e(getResources().getString(R.string.couldnt_find_season)); else startup(); } else if(foundSeason != null) { PlexHttpClient.get(server, foundSeason.key, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { Boolean foundEpisode = false; logger.d("Looking for episode %s", episode); logger.d("videoPlayed: %s", videoPlayed); for(int i=0;i<mc.videos.size();i++) { logger.d("Looking at episode %s", mc.videos.get(i).index); if(mc.videos.get(i).index.equals(episode) && !videoPlayed) { serversSearched++; PlexVideo video = mc.videos.get(i); video.server = server; fetchAndPlayMedia(video); foundEpisode = true; break; } } logger.d("foundEpisode = %s", foundEpisode); if(foundEpisode == false && serversSearched == plexmediaServers.size() && !videoPlayed) { serversSearched++; feedback.e(getResources().getString(R.string.couldnt_find_episode)); return; } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find), queryTerm); } } } private void searchForAlbum(final String artist, final String album) { albums.clear(); if(!artist.equals("")) feedback.m(getString(R.string.searching_for_album), album, artist); else feedback.m(getString(R.string.searching_for_the_album), album); logger.d("Searching for album %s by %s.", album, artist); serversSearched = 0; logger.d("Servers: %d", plexmediaServers.size()); for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.musicSectionsSearched = 0; if(server.musicSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { if(albums.size() == 1) { playAlbum(albums.get(0)); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(albums.size() > 1 ? R.string.found_more_than_one_album : R.string.couldnt_find_album)); return; } } } for(int i=0;i<server.musicSections.size();i++) { String section = server.musicSections.get(i); String path = String.format("/library/sections/%s/search?type=9&query=%s", section, album); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.musicSectionsSearched++; for(int j=0;j<mc.directories.size();j++) { PlexDirectory thisAlbum = mc.directories.get(j); logger.d("Album: %s by %s.", thisAlbum.title, thisAlbum.parentTitle); if(compareTitle(thisAlbum.title, album)) { if(compareTitle(thisAlbum.parentTitle, artist) || artist.equals("")) { logger.d("adding album"); thisAlbum.server = server; albums.add(thisAlbum); } } } if(server.musicSections.size() == server.musicSectionsSearched) { serversSearched++; if(serversSearched == plexmediaServers.size()) { logger.d("found %d albums to play.", albums.size()); if(albums.size() == 1) { playAlbum(albums.get(0)); } else { boolean exactMatch = false; List<PlexDirectory> exactMatchAlbum = new ArrayList<>(); for(int k=0;k<albums.size();k++) { if(albums.get(k).title.toLowerCase().equals(album.toLowerCase())) { logger.d("Found an exact match : %s", album); exactMatch = true; exactMatchAlbum.add(albums.get(k)); } } if(!exactMatch || exactMatchAlbum.size() > 1) { if(queries.size() > 0 && !exactMatch) startup(); else feedback.e(getResources().getString(albums.size() > 1 ? R.string.found_more_than_one_album : R.string.couldnt_find_album)); return; } else if(exactMatchAlbum.size() == 1) { playAlbum(exactMatchAlbum.get(0)); } } } } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if(serversSearched == plexmediaServers.size()) { if(albums.size() == 1) { playAlbum(albums.get(0)); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(albums.size() > 1 ? R.string.found_more_than_one_album : R.string.couldnt_find_album)); return; } } } }); } } private void searchForSong(final String artist, final String track) { serversSearched = 0; feedback.m(getString(R.string.searching_for_album), track, artist); logger.d("Servers: %d", plexmediaServers.size()); for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.musicSectionsSearched = 0; if(server.musicSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { if(tracks.size() > 0) { fetchAndPlayMedia(tracks.get(0)); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_track)); return; } } } for(int i=0;i<server.musicSections.size();i++) { String section = server.musicSections.get(i); String path = String.format("/library/sections/%s/search?type=10&query=%s", section, track); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.musicSectionsSearched++; for(int j=0;j<mc.tracks.size();j++) { PlexTrack thisTrack = mc.tracks.get(j); logger.d("Track: %s by %s.", thisTrack.title, thisTrack.getArtist()); if(compareTitle(thisTrack.getArtist(), artist)) { thisTrack.server = server; tracks.add(thisTrack); } } if(server.musicSections.size() == server.musicSectionsSearched) { serversSearched++; if(serversSearched == plexmediaServers.size()) { logger.d("found %d tracks to play.", tracks.size()); if(tracks.size() == 1) { fetchAndPlayMedia(tracks.get(0)); } else { boolean exactMatch = false; for(int k=0;k<tracks.size();k++) { if(tracks.get(k).getArtist().toLowerCase().equals(artist.toLowerCase())) { exactMatch = true; fetchAndPlayMedia(tracks.get(k)); } } if(!exactMatch) { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_track)); return; } } } } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if(serversSearched == plexmediaServers.size()) { if(tracks.size() > 0) { fetchAndPlayMedia(tracks.get(0)); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_track)); return; } } } }); } } private void searchForArtist(final String artist) { serversSearched = 0; feedback.m(getString(R.string.searching_for_artist), artist); logger.d("Servers: %d", plexmediaServers.size()); for(final PlexServer server : plexmediaServers.values()) { server.findServerConnection(new ActiveConnectionHandler() { @Override public void onSuccess(Connection connection) { server.musicSectionsSearched = 0; if(server.musicSections.size() == 0) { serversSearched++; if(serversSearched == plexmediaServers.size()) { if(tracks.size() > 0) { fetchAndPlayMedia(tracks.get(0)); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_track)); return; } } } for(int i=0;i<server.musicSections.size();i++) { String section = server.musicSections.get(i); String path = String.format("/library/sections/%s/search?type=8&query=%s", section, artist); PlexHttpClient.get(server, path, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { server.musicSectionsSearched++; PlexDirectory theFoundArtist = null; for(int j=0;j<mc.directories.size();j++) { PlexDirectory thisArtist = mc.directories.get(j); // thisTrack.artist = thisTrack.grandparentTitle; // thisTrack.album = thisTrack.parentTitle; logger.d("Artist: %s.", thisArtist.title); if(compareTitle(thisArtist.title, artist)) { thisArtist.server = server; theFoundArtist = thisArtist; break; } } if(theFoundArtist != null) foundArtist(theFoundArtist); else { serversSearched++; if(serversSearched == plexmediaServers.size()) { if (queries.size() > 0) startup(); else feedback.e(String.format(getResources().getString(R.string.couldnt_find_artist), artist)); } } } public void foundArtist(PlexDirectory thisArtist) { playAllFromArtist(thisArtist); } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } } @Override public void onFailure(int statusCode) { serversSearched++; if(serversSearched == plexmediaServers.size()) { if(tracks.size() > 0) { fetchAndPlayMedia(tracks.get(0)); } else { if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_track)); return; } } } }); } } private void playAlbum(final PlexDirectory album) { logger.d("playing album %s", album.key); PlexHttpClient.get(album.server, album.key, new PlexHttpMediaContainerHandler() { @Override public void onSuccess(MediaContainer mc) { if(mc.tracks.size() > 0) { List<PlexTrack> tracks = mc.tracks; for(PlexTrack track : tracks) { track.server = album.server; track.thumb = album.thumb; track.grandparentTitle = album.parentTitle; track.parentTitle = album.title; track.art = album.art; track.grandparentKey = album.parentKey; } if(shuffle) { Collections.shuffle(tracks); } playMedia(tracks.get(0), album); } else { logger.d("Didn't find any tracks"); if(queries.size() > 0) startup(); else feedback.e(getResources().getString(R.string.couldnt_find_album)); return; } } @Override public void onFailure(Throwable error) { feedback.e(getResources().getString(R.string.got_error), error.getMessage()); } }); } /* private void showPlayingTrack(PlexTrack track) { Intent nowPlayingIntent = new Intent(this, NowPlayingActivity.class); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_MEDIA, track); nowPlayingIntent.putExtra(com.atomjack.shared.Intent.EXTRA_CLIENT, client); nowPlayingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(nowPlayingIntent); } */ private class MediaRouterCallback extends MediaRouter.Callback { @Override public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteAdded: %s", route); } @Override public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteSelected: %s", route); // MainActivity.this.onRouteSelected(route); } @Override public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteUnselected: %s", route); // MainActivity.this.onRouteUnselected(route); } } private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { @Override public void onConnected(Bundle connectionHint) { if (mWaitingForReconnect) { mWaitingForReconnect = false; } else { /* try { Cast.CastApi.launchApplication(mApiClient, BuildConfig.CHROMECAST_APP_ID, false) .setResultCallback( new ResultCallback<Cast.ApplicationConnectionResult>() { @Override public void onResult(Cast.ApplicationConnectionResult result) { Status status = result.getStatus(); if (status.isSuccess()) { ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); String sessionId = result.getSessionId(); String applicationStatus = result.getApplicationStatus(); boolean wasLaunched = result.getWasLaunched(); // ... } else { //teardown(); } } }); } catch (Exception e) { logger.d("Failed to launch application", e); } */ } } @Override public void onConnectionSuspended(int cause) { mWaitingForReconnect = true; } } private void sendClientScanIntent() { logger.d("Scanning for clients"); Intent scannerIntent = new Intent(PlexSearchService.this, PlexScannerService.class); scannerIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); scannerIntent.putExtra(PlexScannerService.CLASS, PlexSearchService.class); scannerIntent.setAction(PlexScannerService.ACTION_SCAN_CLIENTS); startService(scannerIntent); } private void onActionFinished(String action, boolean error, PlexMedia media) { if(fromWear) { logger.d("onActionFinished: %s", action); DataMap dataMap = new DataMap(); dataMap.putBoolean(WearConstants.SPEECH_QUERY_RESULT, !error); new SendToDataLayerThread(action, dataMap, this).start(); } } // Feedback class that will also send a message to a connected wear device private class SearchFeedback extends Feedback { public SearchFeedback(Context ctx) { super(ctx); } @Override protected void feedback(String text, boolean error) { super.feedback(text, error); if(VoiceControlForPlexApplication.getInstance().hasWear() && error) { DataMap dataMap = new DataMap(); dataMap.putString(WearConstants.INFORMATION, text); new SendToDataLayerThread(WearConstants.SET_INFO, dataMap, PlexSearchService.this).start(); } } } @Override public void onServiceConnected(ComponentName name, IBinder service) { SubscriptionService.SubscriptionBinder binder = (SubscriptionService.SubscriptionBinder)service; subscriptionService = binder.getService(); logger.d("got subscription service"); subscriptionServiceIsBound = true; if(subscriptionServiceOnConnected != null) subscriptionServiceOnConnected.run(); } @Override public void onServiceDisconnected(ComponentName name) { subscriptionServiceIsBound = false; } }