<NME> IntelligentSorter.java
<BEF> /*
 * Copyright 2015 The SageTV Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package sage.vfs.sort;
import sage.vfs.*;

/**
 *
 * @author Narflex
 */
public class IntelligentSorter extends BasicMediaNodeSorter
{

  /** Creates a new instance of IntelligentSorter */
  public IntelligentSorter(boolean ascending)
  {
    super(ascending);
    mycomp = sage.Seeker.getInstance().getMediaFileComparator(true);
  }

  public int compare(Object o1, Object o2)
  {
    MediaNode m1 = (MediaNode) o1;
    MediaNode m2 = (MediaNode) o2;
    boolean f1 = (m1 == null) ? false : m1.isFolder();
    boolean f2 = (m2 == null) ? false : m2.isFolder();
    if (f1 != f2)
      return f1 ? -1 : 1;
    // Resolve a group into its first child for sorting purposes here
    if (f1 && m1 instanceof GrouperMediaFileNode)
      o1 = ((GrouperMediaFileNode) m1).getFirstDescendant();
    if (f2 && m2 instanceof GrouperMediaFileNode)
      o2 = ((GrouperMediaFileNode) m2).getFirstDescendant();
    o1 = m1.getDataObject();
    o2 = m2.getDataObject();
    if (!(o1 instanceof sage.MediaFile) && !(o1 instanceof sage.Airing))
      o1 = sage.PredefinedJEPFunction.getMediaFileObj(o1);
    if (!(o2 instanceof sage.MediaFile) && !(o2 instanceof sage.Airing))
      o2 = sage.PredefinedJEPFunction.getMediaFileObj(o2);
    return (ascending ? 1 : -1) * mycomp.compare(o1, o2);
  }

  public String getTechnique()
  {
    return MediaNode.SORT_BY_INTELLIGENT;
  }

  public String getName()
  {
    return sage.Sage.rez("Intelligent");
  }

  private java.util.Comparator mycomp;
}

<MSG> Merge pull request #281 from enternoescape/master

Carny multi-threading and more efficient processing.
<DFF> @@ -27,7 +27,7 @@ public class IntelligentSorter extends BasicMediaNodeSorter
   public IntelligentSorter(boolean ascending)
   {
     super(ascending);
-    mycomp = sage.Seeker.getInstance().getMediaFileComparator(true);
+    mycomp = sage.SeekerSelector.getInstance().getMediaFileComparator(true);
   }
 
   public int compare(Object o1, Object o2)

{'commit': 'diff --git a/CHANGELOG.md b/CHANGELOG.md\nindex 19e119c9..c7ecb6ad 100644\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -1,5 +1,13 @@\n # Change Log\n \n+## Version 9.0.15 (??)\n+* Fix: Transcoder crashing on Linux with signal 11.\n+* New: Added new API method to get enabled and disabled favorites.\n+    * public Airing[] GetPotentialFavoriteAirings(Favorite Favorite);\n+* Fix: Aliases without a non-alias would cause an NPE when searching.\n+* Fix: Schedule Direct aliasing logic was applied backwards.\n+* Carny is now multi-threaded and highly optimized.\n+\n ## Version 9.0.14 (2017-03-18)\n * New: Added new API methods for in progress sports tracking using Schedules Direct.\n   * public boolean IsSDEPGServiceAvailable();\ndiff --git a/build.gradle b/build.gradle\nindex f0e76a95..c63bd31a 100755\n--- a/build.gradle\n+++ b/build.gradle\n@@ -312,10 +312,21 @@ def getBuildNumber() {\n \t\tbuildVer = ant.sagebuildnum\t\t\n \t}else if (OperatingSystem.current().isWindows()) {\n \t\t//ensure full path to git\\bin is in the Path Environment variable on windows\n-\t\tbuildVer = [\'sh\', \'-c\', \'git rev-list HEAD --count\'].execute().text.trim()\n+\t\t// 04-24-2017 JS: I changed this to remove the dependency on sh. Also we should note that\n+\t\t// you will get error code 128 if you have git installed, but you did not pull this project\n+\t\t// using git because git will not know where to get the commit count from. This is likely\n+\t\t// true for Linux too.\n+\t\tnew ByteArrayOutputStream().withStream { outputStream ->\n+\t\t\texec {\n+\t\t\t\texecutable \'git\'\n+\t\t\t\targs \'rev-list\', \'HEAD\', \'--count\'\n+\t\t\t\tstandardOutput = outputStream\n+\t\t\t}\n+\t\t\tbuildVer = outputStream.toString().trim()\n+\t\t}\n \t}\n \t\n-\tif (buildVer == null) buildVer=0;\n+\tif (buildVer == null || buildVer == "") buildVer=0;\n \treturn buildVer;\n }\n \ndiff --git a/java/sage/Agent.java b/java/sage/Agent.java\nindex 3fb26a91..b84012b7 100644\n--- a/java/sage/Agent.java\n+++ b/java/sage/Agent.java\n@@ -15,16 +15,24 @@\n  */\n package sage;\n \n+import sage.util.MutableFloat;\n+import sage.util.MutableInteger;\n+\n import java.io.DataInput;\n import java.io.DataOutput;\n import java.io.File;\n import java.io.IOException;\n import java.util.ArrayList;\n import java.util.Arrays;\n+import java.util.Collections;\n import java.util.HashMap;\n+import java.util.HashSet;\n+import java.util.IdentityHashMap;\n+import java.util.Iterator;\n import java.util.List;\n import java.util.Map;\n import java.util.Properties;\n+import java.util.Set;\n import java.util.StringTokenizer;\n import java.util.regex.Matcher;\n import java.util.regex.Pattern;\n@@ -38,8 +46,8 @@ public class Agent extends DBObject implements Favorite\n   private static final int MIN_TOTAL = 2;\n   private static final float MIN_WATCH_PROB = 0f;\n   private static final float MAX_WATCH_PROB = 0.99f;\n-\n-  private static final boolean ENABLE_AGENT_AIRING_CACHE = false;\n+  // Enables checking all airings against the optimized airings. This is for testing only.\n+  static final boolean VERIFY_AIRING_OPTIMIZATION = false;\n \n   private static final int CPU_CONTROL_MOD_COUNT = Sage.getInt("profiler_cpu_mod_count", Sage.EMBEDDED ? 2500 : 25000);\n \n@@ -94,7 +102,7 @@ public class Agent extends DBObject implements Favorite\n     agentID = id;\n     wiz = Wizard.getInstance();\n     createTime = Sage.time();\n-    weakAgents = new int[0];\n+    weakAgents = Pooler.EMPTY_INT_ARRAY;\n     quality = "";\n     autoConvertFormat = "";\n   }\n@@ -138,9 +146,17 @@ public class Agent extends DBObject implements Favorite\n       }\n     }\n     slotType = in.readInt();\n-    weakAgents = new int[in.readInt()];\n-    for (int i = 0; i < weakAgents.length; i++)\n-      weakAgents[i] = readID(in, idMap);\n+    int weakAgentLength = in.readInt();\n+    if (weakAgentLength == 0)\n+    {\n+      weakAgents = Pooler.EMPTY_INT_ARRAY;\n+    }\n+    else\n+    {\n+      weakAgents = new int[weakAgentLength];\n+      for (int i = 0; i < weakAgents.length; i++)\n+        weakAgents[i] = readID(in, idMap);\n+    }\n     if (ver < 0x2D)\n     {\n       int numWeakAirings = in.readInt(); // legacy\n@@ -422,82 +438,351 @@ public class Agent extends DBObject implements Favorite\n       for (i = 0; i < weakAgents.length; i++)\n         if (weakAgents[i] != weakerID)\n           newWeaks[j++] = weakAgents[i];\n-      weakAgents = newWeaks;\n+\n+      if (newWeaks.length == 0)\n+        weakAgents = Pooler.EMPTY_INT_ARRAY;\n+      else\n+        weakAgents = newWeaks;\n       wiz.logUpdate(this, Wizard.AGENT_CODE);\n     } finally {\n       wiz.releaseWriteLock(Wizard.AGENT_CODE);\n     }\n   }\n \n+  private void optimizedFollowsTrend(boolean mustBeViewable, boolean controlCPUUsage,\n+                                     boolean skipKeyword, boolean ignoreDisabledFlag,\n+                                     Map<Integer, Airing[]> airingMap, DBObject allAirings[],\n+                                     Carny.CarnyCache cache, String mapName)\n+  {\n+    // We must have a cache to use this method. This will throw the exception immediately if we do\n+    // not.\n+    final StringBuilder sbCache = cache.sbCache;\n+    final List<Integer> hashes = cache.currentHashes;\n+\n+    // For keyword and timeslot trends with no other limiting factors, we need to check everything\n+    // since we haven\'t worked out a way to optimize this yet. Fortunately these are not as common\n+    // as title and people agents. Skip keyword doesn\'t apply here because it\'s not an optimization\n+    // that implies we will get anything from the map.\n+    // 05-03-2017 JS: I changed the call to hasSpecificCriteria() to just check if we have any\n+    // hashes since it is effectively the same thing, just faster to evaluate.\n+    // 05-04-2017 JS: I removed evaluating anything other than the length of hash since if it is\n+    // zero we have nothing to work with and that always means we need to check all of the provided\n+    // airings. This resulted in a moderate performance bump.\n+    int hashesSize = hashes.size();\n+    boolean fullCheck = hashesSize == 0;\n+\n+    // If an agent has an interest in the hash 0, that means it doesn\'t actually know what it wants\n+    // for at least one of its properties and that means we must do a full check.\n+    // 05/03/2017 JS: We can eliminate this check because getHashes() will return an empty array if\n+    // 0 is any of the hashes that would have been returned. This makes fullCheck already true.\n+    /*if (!fullCheck)\n+    {\n+      for (int i = 0; i < hashes.length; i++)\n+      {\n+        if (hashes[i].equals(0))\n+        {\n+          fullCheck = true;\n+          break;\n+        }\n+      }\n+    }*/\n+\n+    if (fullCheck || airingMap == null)\n+    {\n+      // This is here to help spell out any anything that\'s not being optimized when we are testing.\n+      if (VERIFY_AIRING_OPTIMIZATION && airingMap != null)\n+      {\n+        if (Sage.DBG) System.out.println("Unable to use optimized query: " + toString());\n+      }\n+\n+      for (int i = 0; i < allAirings.length; i++)\n+      {\n+        Airing airing = (Airing) allAirings[i];\n+        if (followsTrend(airing, mustBeViewable, sbCache, skipKeyword, ignoreDisabledFlag))\n+        {\n+          cache.airWorkCache.ensureAddCapacity(1);\n+          cache.airWorkCache.add(airing);\n+        }\n+\n+        if ((i % CPU_CONTROL_MOD_COUNT) == 0 && controlCPUUsage)\n+          try {Thread.sleep(Carny.SLEEP_PERIOD);} catch (Exception e) {}\n+      }\n+    }\n+    else\n+    {\n+      // We need a copy of the list before we make changes to ensure that the long route will be\n+      // starting with the exact same data. We don\'t need to worry about the offset here since if\n+      // we copy everything, the offset still takes us to the same location. This is also only for\n+      // verification.\n+      Carny.CacheList fullTrend = VERIFY_AIRING_OPTIMIZATION ? cache.airWorkCache.copy() : null;\n+\n+      for (int i = cache.hashZero /* 0 or -1 */; i < hashesSize; i++)\n+      {\n+        int airingLength;\n+        Airing airings[];\n+        switch (i)\n+        {\n+          case -1:\n+            // Hash code 0 is special and covers anything that might not have been correctly hashed,\n+            // so we always need to check these.\n+            airings = airingMap.get(0);\n+            if (airings == null)\n+              continue;\n+            airingLength = airings.length;\n+            break;\n+          default:\n+            airings = airingMap.get(hashes.get(i));\n+            if (airings == null)\n+              continue;\n+            airingLength = airings.length;\n+            // If one matches, often most of them will unless it is 0 which is a little more random\n+            // in nature. Testing showed that this consistently reduced time by a measurable amount.\n+            // We could have based this on the largest array when we started, but the reality is\n+            // that we do not know what worker will end up dealing with a large set until it\n+            // happens, so we always deal with last minute adjustments like this.\n+            cache.airWorkCache.ensureAddCapacity(airingLength);\n+        }\n+\n+        // If we get airings from more than one array, we could have duplicates. All of the arrays\n+        // in the map are already binary sorted, so no sort is required initially.\n+        if (cache.airWorkCache.apparentSize() == 0)\n+        {\n+          for (int j = 0; j < airingLength; j++)\n+          {\n+            Airing airing = airings[j];\n+            if (followsTrend(airing, mustBeViewable, sbCache, skipKeyword, ignoreDisabledFlag))\n+            {\n+              // For 0, we just check and resize as we go.\n+              if (i == -1)\n+                cache.airWorkCache.ensureAddCapacity(1);\n+\n+              cache.airWorkCache.add(airing);\n+            }\n+          }\n+        }\n+        else\n+        {\n+          for (int j = 0; j < airingLength; j++)\n+          {\n+            Airing airing = airings[j];\n+            // Inserting airings to maintain binary searching turns out to not be nearly as\n+            // expensive as not being able to do binary searches for duplicate airings. I tried also\n+            // excluding previously processed airings using a HashSet and a binary searched array\n+            // (faster of the two), but it was still faster to re-processing anything that\'s not\n+            // already in the cache and did not clear followsTrend() because of the volume. This is\n+            // also why we don\'t try to consolidate all of the airings before we iterate them here.\n+            int index;\n+            if ((index = cache.airWorkCache.binarySearch(airing)) < 0 &&\n+              followsTrend(airing, mustBeViewable, sbCache, skipKeyword, ignoreDisabledFlag))\n+            {\n+              // For 0, we just check and resize as we go.\n+              if (i == -1)\n+                cache.airWorkCache.ensureAddCapacity(1);\n+\n+              index = -(index + 1);\n+              cache.airWorkCache.add(index, airing);\n+            }\n+          }\n+        }\n+      }\n+\n+      if (VERIFY_AIRING_OPTIMIZATION)\n+      {\n+        Carny.CacheList optAirWorkCache = cache.airWorkCache;\n+        cache.airWorkCache = fullTrend;\n+        // If we don\'t provide a map, a full search is always executed. Also don\'t provide the cache\n+        // or we will overwrite the results of this first run.\n+        optimizedFollowsTrend(mustBeViewable, controlCPUUsage, skipKeyword, ignoreDisabledFlag,\n+          null, allAirings, cache, mapName);\n+\n+        // Restore the old cache and manual wasted value.\n+        cache.airWorkCache = optAirWorkCache;\n+\n+        // We could make this a lot better, but this ia a non-production verification mode.\n+        List<Airing> optList = cache.airWorkCache.getList();\n+        List<Airing> fullTrendList = fullTrend.getList();\n+\n+        if (optAirWorkCache.size != fullTrend.size || !optList.equals(fullTrendList))\n+        {\n+          if (optAirWorkCache.size != fullTrend.size)\n+          {\n+            if (Sage.DBG) System.out.println(\n+              "Optimized size " + optAirWorkCache.size + " != full size " + fullTrend.size);\n+          }\n+\n+          Set<Airing> diffs = new HashSet<>(optList);\n+          diffs.removeAll(fullTrendList);\n+          if (diffs.size() == 0)\n+          {\n+            diffs = new HashSet<>(fullTrendList);\n+            diffs.removeAll(optList);\n+          }\n+          if (diffs.size() > 0)\n+          {\n+            // This process is multi-threaded, so we insert our own new lines to ensure all of\n+            // of the desired details will be adjacent.\n+            StringBuilder stringBuilder = new StringBuilder("Trend optimized map ");\n+            stringBuilder.append(mapName);\n+            stringBuilder.append(" for agent ").append(toString()).append(" is missing ").append("\\r\\n");\n+            stringBuilder.append(diffs).append("\\r\\n");\n+\n+            Set<Integer> usedHashes = new HashSet<>();\n+            Set<Integer> ignoredHashes = new HashSet<>();\n+            Set<Integer> searchedHashes = new HashSet<>();\n+            searchedHashes.addAll(hashes);\n+\n+            main_loop:\n+            for (Iterator<Airing> iterator = diffs.iterator(); iterator.hasNext(); )\n+            {\n+              Airing diff = iterator.next();\n+              Set<Integer> thisAirHashes = new HashSet<>();\n+              int stringReset = stringBuilder.length();\n+              for (Map.Entry<Integer, Airing[]> entry : airingMap.entrySet())\n+              {\n+                if (searchedHashes.contains(entry.getKey()))\n+                {\n+                  usedHashes.add(entry.getKey());\n+                  stringBuilder.setLength(stringReset);\n+                  continue main_loop;\n+                }\n+\n+                // This is used for testing, so we are choosing convenience over performance.\n+                if (Arrays.asList(entry.getValue()).contains(diff))\n+                {\n+                  thisAirHashes.add(entry.getKey());\n+                  stringBuilder.append("Related Airings: ");\n+                  stringBuilder.append(entry.getKey()).append(\':\');\n+                  for (int i = 0; i < entry.getValue().length; i++)\n+                  {\n+                    Airing airing = entry.getValue()[i];\n+                    Show show = airing.getShow();\n+                    if (show != null && searchedHashes.contains(show.title.ignoreCaseHash))\n+                      stringBuilder.append("****");\n+                    stringBuilder.append(show.title.ignoreCaseHash).append(\' \').append(show.title.name).append(\',\');\n+                  }\n+                  stringBuilder.append("\\r\\n");\n+                }\n+              }\n+              ignoredHashes.addAll(thisAirHashes);\n+            }\n+            stringBuilder.append("\\r\\n");\n+            ignoredHashes.removeAll(usedHashes);\n+\n+            // If we have no ignored hashes, then we have something we are not hashing or not\n+            // hashing correctly.\n+            stringBuilder.append("Ignored Hashes: ").append(ignoredHashes).append("\\r\\n");\n+            stringBuilder.append("Used Hashes: ").append(usedHashes).append("\\r\\n");\n+            stringBuilder.append("Searched Hashes: ").append(searchedHashes).append("\\r\\n");\n+            System.out.println(stringBuilder.toString());\n+          }\n+        }\n+      }\n+    }\n+  }\n+\n   /*\n    * The related airings don\'t change unless there\'s an update to the DB, so\n    * cache this call.\n    * This gets called from the AgentBrowser GUI as well as the Carny\'s main\n    * thread, that\'s why it needs to be synced.\n    */\n-  public synchronized Airing[] getRelatedAirings(DBObject[] allAirs, boolean mainCache, boolean controlCPUUsage,\n-      StringBuffer sbCache)\n+\n+  /**\n+   * Get all airings from the provided airings that this agent matches.\n+   *\n+   * @param allAirs All airings to be considered by this agent.\n+   * @param controlCPUUsage If <code>true</code>, this enables CPU usage throttling. This should\n+   *                        only be used when processing a large number of agents and we do not want\n+   *                        to potentially disrupt foreground tasks.\n+   * @param ignoreDisabledFlag If <code>true</code>, treat this agent as if it is enabled, even if it is\n+   *                       not.\n+   * @param sbCache This is an persistent StringBuffer instance for re-use through many iterations.\n+   * @return Airings that match this agent.\n+   */\n+  public Airing[] getRelatedAirings(DBObject[] allAirs, boolean controlCPUUsage,\n+                                    boolean ignoreDisabledFlag, StringBuilder sbCache)\n   {\n-      return getRelatedAirings(allAirs, mainCache, controlCPUUsage, false, sbCache);\n+    boolean legacyKeyword = Sage.getBoolean("use_legacy_keyword_favorites", true);\n+    return getRelatedAirings(\n+      allAirs, controlCPUUsage, ignoreDisabledFlag, legacyKeyword, null, null, sbCache);\n   }\n \n-  public synchronized Airing[] getRelatedAirings(DBObject[] allAirs, boolean mainCache, boolean controlCPUUsage,\n-          boolean ignoreDisabledFlag, StringBuffer sbCache)\n+  /**\n+   * Get all airings from the provided airings that this agent matches.\n+   *\n+   * @param allAirs All airings to be considered by this agent.\n+   * @param controlCPUUsage If <code>true</code>, this enables CPU usage throttling. This should\n+   *                        only be used when processing a large number of agents and we do not want\n+   *                        to potentially disrupt foreground tasks.\n+   * @param ignoreDisabledFlag If <code>true</code>, treat this agent as if it is enabled, even if it is\n+   *                       not.\n+   * @param legacyKeyword Do not use Lucene to optimize keyword searches.\n+   * @param cache This is an optional shared {@link sage.Carny.CarnyCache} object.\n+   * @param sbCache This is an persistent StringBuffer instance for re-use through many iterations.\n+   * @return Airings that match this agent.\n+   */\n+  public Airing[] getRelatedAirings(DBObject[] allAirs, boolean controlCPUUsage,\n+                                    boolean ignoreDisabledFlag, boolean legacyKeyword,\n+                                    Map<Integer, Airing[]> airingMap, Carny.CarnyCache cache,\n+                                    StringBuilder sbCache)\n   {\n-    if (allAirs == null) return Pooler.EMPTY_AIRING_ARRAY;\n+    if (allAirs == null)\n+      return Pooler.EMPTY_AIRING_ARRAY;\n+    if (sbCache == null)\n+      sbCache = new StringBuilder();\n \n-    //Short circuit this if the agent is disabled (and we don\'t want to ignore that fact)\n-    if(!ignoreDisabledFlag && testAgentFlag(DISABLED_FLAG)) return Pooler.EMPTY_AIRING_ARRAY;\n+    List<Airing> rv = null;\n+    if (cache == null)\n+      rv = new ArrayList<Airing>();\n+    else\n+      cache.airWorkCache.clear();\n \n-    if (ENABLE_AGENT_AIRING_CACHE)\n+    boolean keywordTest = !legacyKeyword && (this.agentMask&(KEYWORD_MASK)) == (KEYWORD_MASK);\n+    if(keywordTest)\n     {\n-      if (mainCache)\n-      {\n-        if (relAirCache != null && cacheTimestamp > wiz.getLastModified())\n-          return relAirCache;\n-        cacheTimestamp = Sage.time();\n-      }\n-      else\n-      {\n-        if (relAirCache2 != null && cacheTimestamp2 > wiz.getLastModified())\n-          return relAirCache2;\n-        cacheTimestamp2 = Sage.time();\n-      }\n-    }\n-\n-    ArrayList<Airing> rv = new ArrayList<Airing>();\n-    boolean keywordTest = (this.agentMask&(KEYWORD_MASK)) == (KEYWORD_MASK) &&\n-        !Sage.getBoolean("use_legacy_keyword_favorites", true);\n-    if(keywordTest) {\n       // If we\'re only doing a keyword mask, speed it up via Lucene\n       Show[] shows = wiz.searchShowsByKeyword(getKeyword());\n-      for (Show show : shows) {\n+      for (Show show : shows)\n+      {\n         Airing[] airings = wiz.getAirings(show, 0);\n-        for(Airing a : airings) {\n+        if (cache != null)\n+          cache.airWorkCache.ensureAddCapacity(airings.length);\n+        for (Airing a : airings)\n+        {\n           if (followsTrend(a, true, sbCache, true, ignoreDisabledFlag))\n-            rv.add(a);\n+          {\n+            if (cache != null)\n+              cache.airWorkCache.add(a);\n+            else\n+              rv.add(a);\n+          }\n         }\n       }\n-    } else {\n-      for (int i = 0; i < allAirs.length; i++)\n-      {\n-        Airing a = (Airing) allAirs[i];\n-        if (a == null) continue;\n-        if (followsTrend(a, true, sbCache, false, ignoreDisabledFlag))\n-          rv.add(a);\n-        if ((i % CPU_CONTROL_MOD_COUNT) == 0 && controlCPUUsage)\n-          try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n-      }\n     }\n-    if (ENABLE_AGENT_AIRING_CACHE)\n+    else\n     {\n-      if (mainCache)\n-        return (relAirCache = rv.toArray(Pooler.EMPTY_AIRING_ARRAY));\n+      if (cache != null)\n+      {\n+        optimizedFollowsTrend(true, controlCPUUsage, false, ignoreDisabledFlag, airingMap,\n+          allAirs, cache, "allAirs");\n+      }\n       else\n-        return (relAirCache2 = rv.toArray(Pooler.EMPTY_AIRING_ARRAY));\n+      {\n+        for (int i = 0; i < allAirs.length; i++)\n+        {\n+          Airing a = (Airing) allAirs[i];\n+          if (a == null) continue;\n+          if (followsTrend(a, true, sbCache, true, ignoreDisabledFlag))\n+            rv.add(a);\n+          if ((i % CPU_CONTROL_MOD_COUNT) == 0 && controlCPUUsage)\n+            try {Thread.sleep(Carny.SLEEP_PERIOD);} catch (Exception e) {}\n+        }\n+      }\n     }\n-    else\n-      return rv.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+\n+    // We don\'t return anything if we used the cache. The data is in the cache. This needs to either\n+    // be more elegant or the cache should always contain the return.\n+    return cache != null ? Pooler.EMPTY_AIRING_ARRAY : rv.toArray(Pooler.EMPTY_AIRING_ARRAY);\n   }\n \n   boolean validate()\n@@ -522,9 +807,6 @@ public class Agent extends DBObject implements Favorite\n \n   synchronized void update(DBObject fromMe)\n   {\n-    // Clear the airings caches\n-    cacheTimestamp = cacheTimestamp2 = 0;\n-\n     Agent bond = (Agent) fromMe;\n     agentMask = bond.agentMask;\n     createTime = bond.createTime;\n@@ -539,7 +821,7 @@ public class Agent extends DBObject implements Favorite\n     person = bond.person;\n     slotType = bond.slotType;\n     timeslots = (bond.timeslots == null ? null : ((int[]) bond.timeslots.clone()));\n-    weakAgents = bond.weakAgents.clone();\n+    weakAgents = bond.weakAgents.length == 0 ? Pooler.EMPTY_INT_ARRAY : bond.weakAgents.clone();\n     quality = bond.quality;\n     autoConvertFormat = bond.autoConvertFormat;\n     autoConvertDest = bond.autoConvertDest;\n@@ -559,6 +841,8 @@ public class Agent extends DBObject implements Favorite\n   public boolean isReRunsOnly() { return (agentMask & RERUN_MASK) == RERUN_MASK; }\n   public boolean firstRunsAndReruns() { return !isFirstRunsOnly() && !isReRunsOnly(); }\n   public String getTitle() { return (title == null) ? "" : title.name; }\n+  // Title to be compared must be lowercase.\n+  public boolean isSameTitle(String title) { return (title == null) ? false : title.equalsIgnoreCase(title); }\n   public String getCategory() { return (category == null) ? "" : category.name; }\n   public String getSubCategory() { return (subCategory == null) ? "" : subCategory.name; }\n   public String getPerson() { return (person == null) ? "" : person.name; }\n@@ -573,36 +857,41 @@ public class Agent extends DBObject implements Favorite\n   public int getSlotType() { return slotType; }\n   public String getKeyword() { return keyword; }\n \n-  public boolean followsTrend(Airing air, boolean mustBeViewable, StringBuffer sbCache)\n+  public boolean followsTrend(Airing air, boolean mustBeViewable, StringBuilder sbCache)\n   {\n     return followsTrend(air, mustBeViewable, sbCache, false, false);\n   }\n \n-  public boolean followsTrend(Airing air, boolean mustBeViewable, StringBuffer sbCache, boolean skipKeyword)\n+  public boolean followsTrend(Airing air, boolean mustBeViewable, StringBuilder sbCache,\n+                              boolean skipKeyword)\n   {\n       return followsTrend(air, mustBeViewable, sbCache, skipKeyword, false);\n   }\n+\n   /**\n-   * Determine if the given airing meets the criteria for this Agent. (i.e. could the given Airing be scheduled because\n-   * of this Agent)\n+   * Determine if the given airing meets the criteria for this Agent. (i.e. could the given Airing\n+   * be scheduled because of this Agent)\n    *\n    * @param air The Airing to be tested\n-   * @param mustBeViewable If true, the Airing must be viewable for this method to return true.  Viewable means a\n-   * recording that can be watched, or a channel that can be viewed.\n-   * @param sbCache A StringBuffer to be used by this method.  If null a new StringBuffer will be created.  If non-null\n-   * the buffer will be cleared and use.  When calling this method in a loop, the same StringBuffer can be used for each\n-   * call to limit object creation and memory use.\n+   * @param mustBeViewable If true, the Airing must be viewable for this method to return true.\n+   *                       Viewable means a recording that can be watched, or a channel that can be\n+   *                       viewed.\n+   * @param sbCache A StringBuffer to be used by this method.  If null a new StringBuffer will be\n+   *                created. If non-null the buffer will be cleared and use.  When calling this\n+   *                method in a loop, the same StringBuffer can be used for each call to limit object\n+   *                creation and memory use.\n    * @param skipKeyword If true, keyword matching is not considered\n    * @param ignoreDisabledFlag If true, treat this agent as if it is enabled, even if it isn\'t.\n-   * @return true if the given Airing matches this Agent (given the parameter criteria) , false otherwise.\n+   * @return true if the given Airing matches this Agent (given the parameter criteria), false otherwise.\n    */\n-  /*\n+  public boolean followsTrend(Airing air, boolean mustBeViewable, StringBuilder sbCache,\n+                              boolean skipKeyword, boolean ignoreDisabledFlag)\n+  {\n+    /*\n    * TODO(codefu): skipKeyword is a hack before showcase. It works since the other flags are AND\n    * tested; but we can have Lucene do this for us\n    */\n-  public boolean followsTrend(Airing air, boolean mustBeViewable, StringBuffer sbCache, boolean skipKeyword,\n-          boolean ignoreDisabledFlag)\n-  {\n+\n     if (air == null) return false;\n     Show s = air.getShow();\n     if (s == null) return false;\n@@ -612,7 +901,7 @@ public class Agent extends DBObject implements Favorite\n         return false;\n     // Do not be case sensitive when checking titles!! We got a bunch of complaints about this on our forums.\n     // Don\'t let null titles match all the Favorites!\n-    if (title != null && (s.title == null || (s.title != null && title != s.title && !title.toString().equalsIgnoreCase(s.title.toString()))))\n+    if (title != null && (s.title == null || !title.equalsIgnoreCase(s.title)))\n       return false;\n     if ((agentMask & FIRSTRUN_MASK) == FIRSTRUN_MASK && !air.isFirstRun())\n       return false;\n@@ -622,7 +911,8 @@ public class Agent extends DBObject implements Favorite\n     {\n       int i = 0;\n       for (; i < s.people.length; i++)\n-        if ((person == s.people[i] || person.name.equalsIgnoreCase(s.people[i].name)) && (role == Show.ALL_ROLES || role == 0 || role == s.roles[i]))\n+        if ((person == s.people[i] || person.equalsIgnoreCase(s.people[i])) &&\n+          (role == Show.ALL_ROLES || role == 0 || role == s.roles[i]))\n           break;\n       if (i == s.people.length) return false;\n     }\n@@ -643,9 +933,11 @@ public class Agent extends DBObject implements Favorite\n           return false;\n       }\n     }\n-    if (chanName.length() > 0 && (air.getChannel() == null || !chanNameMatches(air.getChannel().name)))\n+    // We were getting the channel up to four times more than we actually need.\n+    Channel channel = null;\n+    if (chanName.length() > 0 && ((channel = air.getChannel()) == null || !chanNameMatches(channel.name)))\n       return false;\n-    if (network != null && (air.getChannel() == null || network != air.getChannel().network))\n+    if (network != null && ((channel == null && ((channel = air.getChannel()) == null)) || network != channel.network))\n       return false;\n     if (rated != null && rated != s.rated)\n       return false;\n@@ -671,7 +963,8 @@ public class Agent extends DBObject implements Favorite\n     if ((agentMask & LOVE_MASK) == 0 && (agentMask & TITLE_MASK) == 0)\n     {\n       // Non-title tracks only match English language shows\n-      if (s.language != null && !"English".equalsIgnoreCase(s.language.name) && !"en".equalsIgnoreCase(s.language.name))\n+      if (s.language != null && s.language.name != null &&\n+        !s.language.equalsIgnoreCase("english") && !s.language.equalsIgnoreCase("en"))\n         return false;\n     }\n \n@@ -679,15 +972,15 @@ public class Agent extends DBObject implements Favorite\n     if (mustBeViewable && wiz.getFileForAiring(air) == null && !air.isViewable())\n       return false;\n \n-    if (skipKeyword == false && keyword.length() > 0)\n+    if (!skipKeyword && keyword.length() > 0)\n     {\n       boolean titleOnly = keyword.startsWith("TITLE:");\n       String currKeyword = titleOnly ? keyword.substring("TITLE:".length()).trim() : keyword;\n       // The fields in Show that we can test against are:\n       // Title, Episode, Description, Year, People, category, subcategory, bonuses, ers, language\n-      StringBuffer fullShowTest;\n+      StringBuilder fullShowTest;\n       if (sbCache == null)\n-        fullShowTest = new StringBuffer(s.getTitle());\n+        fullShowTest = new StringBuilder(s.getTitle());\n       else\n       {\n         sbCache.setLength(0);\n@@ -713,7 +1006,10 @@ public class Agent extends DBObject implements Favorite\n         fullShowTest.append(\'|\'); s.appendBonusesString(fullShowTest);\n         fullShowTest.append(\'|\'); s.appendExpandedRatingsString(fullShowTest);\n         fullShowTest.append(\'|\'); fullShowTest.append(s.getLanguage());\n-        fullShowTest.append(\'|\'); fullShowTest.append(air.getChannelName());\n+        fullShowTest.append(\'|\'); fullShowTest.append(channel != null ? channel.name : air.getChannelName());\n+        // This is a small point of contention because it uses the String resolver. If someone has\n+        // many threads running and many keyword favorites, this will collide a lot more than you\n+        // want on Part_Of_Parts.\n         fullShowTest.append(\'|\'); air.appendMiscInfo(fullShowTest);\n         fullShowTest.append(\'|\'); fullShowTest.append(s.getExternalID());\n       }\n@@ -821,20 +1117,32 @@ public class Agent extends DBObject implements Favorite\n   }\n \n   // Returns false if it\'s an agent that shouldn\'t exist\n-  boolean calcWatchProb(boolean controlCPUUsage, Airing[] airWorkCache, StringBuffer sbCache)\n+  boolean calcWatchProb(boolean controlCPUUsage, Airing[] watchAirs, Airing[] wastedAirs,\n+                        boolean aggressiveNegativeProfiling, Carny.CarnyCache cache)\n   {\n     if ((agentMask & LOVE_MASK) == LOVE_MASK)\n     {\n       watchProb = 1;\n       negator = false;\n \n-      // Save some last-used values to display for the agent; indicatre as not set.\n+      // Save some last-used values to display for the agent; indicate as not set.\n       lastnumWatchedAirs = -1;\n       lastnumWastedAirs = -1;\n       lastnumManualWaste = -1;\n \n       return true;\n     }\n+\n+    final Map<Integer, Airing[]> watchAirsMap = cache.watchAirsMap;;\n+    final Map<Integer, Wasted[]> wastedAirsMap = cache.wastedAirsMap;;\n+    final StringBuilder sbCache = cache.sbCache;\n+    final List<Integer> hashes = cache.currentHashes;\n+    final boolean unoptimized = hashes.size() == 0 || !cache.useMaps;\n+\n+    // NOTE: We never call this method using an offset, so we can make some assumptions. If this\n+    // changes in the future, those assumptions need to be changed to account for the offset.\n+    cache.airWorkCache.clear();\n+\n     // There\'s three different things that affect this calculation.\n     // 1. The Airings that are declared Watched that follow this trend\n     // 2. The Airings that are Wasted that follow this trend\n@@ -846,56 +1154,170 @@ public class Agent extends DBObject implements Favorite\n      * end of the array when we don\'t have overlap. We use a \'mergesort\' type of scan to prevent overlap between\n      * watched and wasted airings. We\'ll also need to track how many of them were Watched and how many were Wasted.\n      */\n-    int numWatchedAirs = 0;\n-    int numWastedAirs = 0;\n-    DBObject[] watches = wiz.getRawAccess(Wizard.WATCH_CODE, (byte) 0);\n-    for (int i = 0; i < watches.length; i++)\n+    if (unoptimized)\n     {\n-      Watched currWatch = (Watched) watches[i];\n-      if (watches[i] != null && BigBrother.isFullWatch(currWatch))\n+      DBObject[] watches = wiz.getRawAccess(Wizard.WATCH_CODE, (byte) 0);\n+      cache.airWorkCache.ensureAddCapacity(watches.length);\n+\n+      for (int i = 0; i < watches.length; i++)\n       {\n-        Airing testA = currWatch.getAiring();\n-        if (followsTrend(testA, false, sbCache))\n+        Watched currWatch = (Watched) watches[i];\n+        if (watches[i] != null && BigBrother.isFullWatch(currWatch))\n         {\n-          airWorkCache[numWatchedAirs++] = testA;\n+          Airing testA = currWatch.getAiring();\n+          if (followsTrend(testA, false, sbCache))\n+          {\n+            cache.airWorkCache.ensureAddCapacity(1);\n+            cache.airWorkCache.add(testA);\n+          }\n         }\n+        if (controlCPUUsage && (i % CPU_CONTROL_MOD_COUNT) == 0)\n+          try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n       }\n-      if (controlCPUUsage && (i % CPU_CONTROL_MOD_COUNT) == 0)\n-        try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n+\n+      cache.airWorkCache.sort();\n+    }\n+    else\n+    {\n+      // These results will already be sorted by ID.\n+      optimizedFollowsTrend(false, controlCPUUsage, false, false, watchAirsMap, watchAirs,\n+        cache, "watchAirsMap");\n     }\n-    Arrays.sort(airWorkCache, 0, numWatchedAirs, DBObject.ID_COMPARATOR);\n-    DBObject[] waste = wiz.getRawAccess(Wizard.WASTED_CODE, (byte) 0);\n+\n+    int numWatchedAirs = cache.airWorkCache.size;\n+\n     int numManualWaste = 0;\n-    int watchAirCompareIdx = 0;\n-    for (int i = 0; i < waste.length; i++)\n+    if (unoptimized)\n     {\n-      Wasted currWaste = (Wasted) waste[i];\n-      if (waste[i] != null)\n+      DBObject[] waste = wiz.getRawAccess(Wizard.WASTED_CODE, (byte) 0);\n+      cache.airWorkCache.ensureAddCapacity(waste.length);\n+      for (int i = 0; i < waste.length; i++)\n       {\n-        Airing testW = currWaste.getAiring();\n-        if (followsTrend(testW, false, sbCache))\n+        Wasted currWaste = (Wasted) waste[i];\n+        if (waste[i] != null)\n+        {\n+          Airing testW = currWaste.getAiring();\n+          if (followsTrend(testW, false, sbCache))\n+          {\n+            if (currWaste.manual)\n+              numManualWaste++;\n+            if (cache.airWorkCache.binarySearch(numWatchedAirs, testW) < 0)\n+              cache.airWorkCache.add(testW);\n+          }\n+        }\n+        if (controlCPUUsage && (i % CPU_CONTROL_MOD_COUNT) == 0)\n+          try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n+      }\n+    }\n+    else\n+    {\n+      // If we only get one array, we can forgo using a set. This happens fairly often since many\n+      // times an agent only has one interest.\n+      Wasted[] oneWasted = null;\n+      Set<Wasted> matchedWastedAirs = null;\n+\n+\n+      for (int i = 0, hashesSize = hashes.size(); i < hashesSize; i++)\n+      {\n+        Integer hash = hashes.get(i);\n+        Wasted[] wasteds = wastedAirsMap.get(hash);\n+        if (oneWasted == null)\n+        {\n+          if (wasteds != null)\n+          {\n+            oneWasted = wasteds;\n+            cache.airWorkCache.ensureAddCapacity(wasteds.length);\n+          }\n+        }\n+        else if (wasteds != null)\n         {\n-          if (currWaste.manual)\n-            numManualWaste++;\n-          // Check for overlap with the Watches before we add it\n-          int idDiff = 1;\n-          while (watchAirCompareIdx < numWatchedAirs)\n+          if (matchedWastedAirs == null)\n           {\n-            idDiff = airWorkCache[watchAirCompareIdx].getID() - testW.getID();\n-            if (idDiff < 0)\n+            // We are using identity because it doesn\'t create nodes and Wasted objects are always\n+            // the same object when they are the same Wasted airing. There will not be other objects\n+            // that could be equal. This map is actually faster more often than not in this specific\n+            // situation with the bonus of not creating excess objects.\n+            matchedWastedAirs = Collections.newSetFromMap(\n+              new IdentityHashMap<Wasted, Boolean>(oneWasted.length + wasteds.length));\n+            // HashSet bulk add is one by one just like this. There is no advantage to converting\n+            // to a list; just more GC collection.\n+            for (int j = 0, oneWastedLength = oneWasted.length; j < oneWastedLength; j++)\n             {\n-              watchAirCompareIdx++;\n-              idDiff = 1;\n+              matchedWastedAirs.add(oneWasted[j]);\n             }\n-            else\n-              break;\n           }\n-          if (idDiff > 0)\n-            airWorkCache[numWatchedAirs + numWastedAirs++] = testW;\n+\n+          for (int j = 0, wastedsLength = wasteds.length; j < wastedsLength; j++)\n+          {\n+            matchedWastedAirs.add(wasteds[j]);\n+          }\n+          cache.airWorkCache.ensureAddCapacity(wasteds.length);\n+        }\n+      }\n+\n+      // Sizes of 0 take the unoptimized path. Sizes of 1 do not need to be deduplicated, so we can\n+      // process the single array if it exists directly.\n+      if (matchedWastedAirs != null)\n+      {\n+        int i = 0;\n+        for (Wasted currWaste : matchedWastedAirs)\n+        {\n+          Airing testW = currWaste.getAiring();\n+          if (testW == null)\n+            continue;\n+          if (followsTrend(testW, false, sbCache))\n+          {\n+            if (currWaste.manual)\n+              numManualWaste++;\n+            if (cache.airWorkCache.binarySearch(numWatchedAirs, testW) < 0)\n+              cache.airWorkCache.add(testW);\n+          }\n+\n+          if (controlCPUUsage && (i++ % CPU_CONTROL_MOD_COUNT) == 0)\n+            try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n+        }\n+      }\n+      else if (oneWasted != null)\n+      {\n+        int airingsLength = oneWasted.length;\n+        for (int i = 0; i < airingsLength; i++)\n+        {\n+          Wasted currWaste = oneWasted[i];\n+          Airing testW = currWaste.getAiring();\n+          if (testW == null)\n+            continue;\n+          if (followsTrend(testW, false, sbCache))\n+          {\n+            if (currWaste.manual)\n+              numManualWaste++;\n+            if (cache.airWorkCache.binarySearch(numWatchedAirs, testW) < 0)\n+              cache.airWorkCache.add(testW);\n+          }\n+\n+          if (controlCPUUsage && (i % CPU_CONTROL_MOD_COUNT) == 0)\n+            try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n         }\n       }\n-      if (controlCPUUsage && (i % CPU_CONTROL_MOD_COUNT) == 0)\n-        try{Thread.sleep(Carny.SLEEP_PERIOD);}catch(Exception e){}\n+    }\n+    // This is correct because the air isn\'t considered wasted if it was also fully watched. We\n+    // accounted for manual waste (e.g. Don\'t Like) above. This is how the old behavior effectively\n+    // worked.\n+    int numWastedAirs = cache.airWorkCache.size - numWatchedAirs;\n+\n+    if (VERIFY_AIRING_OPTIMIZATION)\n+    {\n+      Set<Airing> test = new HashSet<>(cache.airWorkCache.getList());\n+      if (test.size() != cache.airWorkCache.apparentSize())\n+      {\n+        System.out.println("WARNING: List contains redundant values: " + test.size() + " != " + cache.airWorkCache.apparentSize());\n+        StringBuilder stringBuilder = new StringBuilder("Redundant list: ");\n+        for (int i = cache.airWorkCache.offset; i < cache.airWorkCache.size; i++)\n+        {\n+          Airing object = cache.airWorkCache.data[i];\n+          stringBuilder.append(object.getID()).append(\',\');\n+        }\n+        System.out.println(stringBuilder);\n+      }\n     }\n \n     float watchCount = 0;\n@@ -903,50 +1325,75 @@ public class Agent extends DBObject implements Favorite\n \n     // Make all non-title based agents title unique for tracking\n     boolean titleUniqueCount = (agentMask & TITLE_MASK) == 0;\n-    Map<Show, List<Airing>> doneShowMap = new HashMap<Show, List<Airing>>();\n-    Map<Stringer, Float> titleWatchMap = null;\n-    Map<Stringer, Integer> titleAllMap = null;\n+    int cacheSize = cache.airWorkCache.size;\n+    // It\'s faster not to cache and clear this map. This appears to be due to the number of shows\n+    // being stored that need to be cleared.\n+    // Single Airing or ArrayList<Airing>\n+    Map<Show, Object> doneShowMap = new HashMap<>((int)(cacheSize / 0.75) + 1);\n+    Map<Stringer, MutableFloat> titleWatchMap = null;\n+    Map<Stringer, MutableInteger> titleAllMap =  null;\n+\n     if (titleUniqueCount)\n     {\n-      titleWatchMap = new HashMap<Stringer, Float>();\n-      titleAllMap = new HashMap<Stringer, Integer>();\n+      titleWatchMap = cache.titleWatchMap;\n+      titleWatchMap.clear();\n+      titleAllMap = cache.titleAllMap;\n+      titleAllMap.clear();\n     }\n-    for (int i = 0; i < numWatchedAirs + numWastedAirs; i++)\n+\n+    next_air:\n+    for (int i = 0; i < cacheSize; i++)\n     {\n-      Airing currAir = airWorkCache[i];\n+      Airing currAir = cache.airWorkCache.data[i];\n       Show currShow = currAir.getShow();\n       if (currShow == null) continue;\n       Stringer theTit = null;\n       if (titleUniqueCount)\n         theTit = currShow.title;\n-      List<Airing> airSet = doneShowMap.get(currShow);\n-      if (airSet != null)\n+      Object airSet = doneShowMap.get(currShow);\n+      if (airSet instanceof Airing)\n       {\n-        boolean didIt = false;\n-        for (int j = 0; j < airSet.size() && !didIt; j++)\n+        if (BigBrother.areSameShow((Airing) airSet, currAir, true, sbCache))\n+          continue;\n+\n+        ArrayList<Airing> newList = new ArrayList<>();\n+        newList.add((Airing) airSet);\n+        newList.add(currAir);\n+        doneShowMap.put(currShow, newList);\n+      }\n+      // For some reason this didn\'t work correctly when it was instanceof List.\n+      else if (airSet instanceof ArrayList)\n+      {\n+        for (int j = 0; j < ((ArrayList)airSet).size(); j++)\n         {\n-          if (BigBrother.areSameShow(airSet.get(j), currAir, true))\n-            didIt = true;\n+          if (BigBrother.areSameShow((Airing)((ArrayList)airSet).get(j), currAir, true, sbCache))\n+            continue next_air;\n         }\n-        if (didIt)\n-          continue;\n       }\n-      else\n+      else // Must be null.\n       {\n-        airSet = new ArrayList<Airing>();\n-        doneShowMap.put(currShow, airSet);\n+        doneShowMap.put(currShow, currAir);\n       }\n-      airSet.add(currAir);\n \n       if (i < numWatchedAirs)\n       {\n         if (titleUniqueCount)\n         {\n-          Float theFloat = titleWatchMap.get(theTit);\n-          titleWatchMap.put(theTit, 1 + ((theFloat == null) ? 0 : theFloat));\n+          // Floats are not cached in any way like Integers, so this ends up creating tons of\n+          // objects when all we really need is a mutable float object.\n+          MutableFloat theFloat = titleWatchMap.get(theTit);\n+          //titleWatchMap.put(theTit, 1 + ((theFloat == null) ? 0 : theFloat));\n+          if (theFloat == null)\n+            titleWatchMap.put(theTit, new MutableFloat(1));\n+          else\n+            theFloat.increment(1);\n \n-          Integer theInt = titleAllMap.get(theTit);\n-          titleAllMap.put(theTit, 1 + ((theInt == null) ? 0 : theInt));\n+          MutableInteger theInt = titleAllMap.get(theTit);\n+          //titleAllMap.put(theTit, 1 + ((theInt == null) ? 0 : theInt));\n+          if (theInt == null)\n+            titleAllMap.put(theTit, new MutableInteger(1));\n+          else\n+            theInt.increment(1);\n         }\n         else\n         {\n@@ -959,8 +1406,12 @@ public class Agent extends DBObject implements Favorite\n       {\n         if (titleUniqueCount)\n         {\n-          Integer theInt = titleAllMap.get(theTit);\n-          titleAllMap.put(theTit, 1 + ((theInt == null) ? 0 : theInt));\n+          MutableInteger theInt = titleAllMap.get(theTit);\n+          //titleAllMap.put(theTit, 1 + ((theInt == null) ? 0 : theInt));\n+          if (theInt == null)\n+            titleAllMap.put(theTit, new MutableInteger(1));\n+          else\n+            theInt.increment(1);\n         }\n         else\n         {\n@@ -971,12 +1422,12 @@ public class Agent extends DBObject implements Favorite\n     int realTotalCount;\n     if (titleUniqueCount)\n     {\n-      for (Map.Entry<Stringer, Integer> ent : titleAllMap.entrySet())\n+      for (Map.Entry<Stringer, MutableInteger> ent : titleAllMap.entrySet())\n       {\n-        int total = ent.getValue();\n-        Float currW = titleWatchMap.get(ent.getKey());\n+        int total = ent.getValue().getValue();\n+        MutableFloat currW = titleWatchMap.get(ent.getKey());\n         if (currW != null)\n-          watchCount += currW / total;\n+          watchCount += currW.getValue() / total;\n         totalCount++;\n       }\n       realTotalCount = totalCount;\n@@ -996,19 +1447,23 @@ public class Agent extends DBObject implements Favorite\n     // If there are more Don\'t Likes than Watched, set Watch Prob to 0 so this agent\n     // won\'t be the cause of a recording w/o stopping some other agent from causing a recording.\n     if ( (numWatchedAirs == 0 && numWastedAirs > 1) || (numManualWaste > numWatchedAirs) )\n-        watchProb = 0;\n+      watchProb = 0;\n \n     // For a Title agent (or if aggressive negative profiling is on), then check whether\n     // this agent should also prevent any other agent from recordings something; i.e.: set negator to true.\n     negator = ((agentMask & TITLE_MASK) != 0 || Sage.getBoolean("aggressive_negative_profiling", false)) &&\n-        ((numWatchedAirs == 0 && numWastedAirs > 1) ||\n-            (numManualWaste > numWatchedAirs));\n+      ((numWatchedAirs == 0 && numWastedAirs > 1) ||\n+        (numManualWaste > numWatchedAirs));\n \n     // Save some last-used values to display for the agent.\n     lastnumWatchedAirs = numWatchedAirs;\n     lastnumWastedAirs = numWastedAirs;\n     lastnumManualWaste = numManualWaste;\n \n+    lastRealTotalCount = realTotalCount;\n+    lastTotalCount = totalCount;\n+    lastWatchCount = watchCount;\n+\n     // We need two data points to exist if we\'re actor based\n     if (realTotalCount == 0) return false;\n     if ((agentMask & TITLE_MASK) != 0) return true;\n@@ -1017,6 +1472,10 @@ public class Agent extends DBObject implements Favorite\n     return (watchCount >= 2);\n   }\n \n+  int lastRealTotalCount;\n+  int lastTotalCount;\n+  float lastWatchCount;\n+\n   public String getTypeName()\n   {\n     if (keyword != null && keyword.length() > 0)\n@@ -1163,8 +1622,6 @@ public class Agent extends DBObject implements Favorite\n     return sb.toString().trim();\n   }\n \n-  void clearCache() { relAirCache = null; }\n-\n   public boolean isNegativeNelly()\n   {\n     return negator;\n@@ -1339,6 +1796,98 @@ public class Agent extends DBObject implements Favorite\n     return (Properties) favProps.clone();\n   }\n \n+  // This helps us not create any more Integer objects than we need to.\n+  private void addHash(int intHash, List<Integer> list, int bitShift)\n+  {\n+    int hash = intHash >>> bitShift;\n+    int low = 0;\n+    int high = list.size() - 1;\n+\n+    while (low <= high)\n+    {\n+      int mid = (low + high) >>> 1;\n+      int midVal = list.get(mid);\n+\n+      if (midVal < hash)\n+        low = mid + 1;\n+      else if (midVal > hash)\n+        high = mid - 1;\n+      else\n+        return;\n+    }\n+    list.add(low, hash);\n+  }\n+\n+  // 05/03/2017 JS: I changed this from caching this value to just re-creating it every time. The\n+  // performance is break even since we only use these hashes once per cycle and it doesn\'t appear\n+  // to be worth the memory usage. Since we aren\'t caching, we also use Integer since we will be\n+  // using this against a HashMap and this will keep us from autoboxing in the process.\n+  void getHashes(List<Integer> searchHashes, int bitShift)\n+  {\n+    // We don\'t need to include 0 because that\'s always assumed in look ups. If we do return 0, that\n+    // means this agent isn\'t sure what it needs, but the inverse is acceptable because that just\n+    // means the airing doesn\'t know what it matches and it will be tested on all of the agents.\n+    searchHashes.clear();\n+\n+    if (title != null)\n+    {\n+      searchHashes.add((title.ignoreCaseHash >>> bitShift));\n+    }\n+\n+    if (person != null)\n+    {\n+      addHash(person.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    if (category != null)\n+    {\n+      addHash(category.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    if (subCategory != null)\n+    {\n+      addHash(subCategory.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    if (chanName.length() > 0)\n+    {\n+      addHash(chanName.hashCode(), searchHashes, bitShift);\n+    }\n+\n+    if (chanNames != null && chanNames.length > 0)\n+    {\n+      for (String chanName : chanNames)\n+      {\n+        addHash(chanName.hashCode(), searchHashes, bitShift);\n+      }\n+    }\n+\n+    if (network != null)\n+    {\n+      addHash(network.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    if (rated != null)\n+    {\n+      addHash(rated.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    if (year != null)\n+    {\n+      addHash(year.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    if (pr != null)\n+    {\n+      addHash(pr.ignoreCaseHash, searchHashes, bitShift);\n+    }\n+\n+    // This will ensure that we do a full search since 0 means at least one of our items doesn\'t\n+    // have a "valid" hash.\n+    if (searchHashes.contains(0))\n+      searchHashes.clear();\n+  }\n+\n   final int agentID;\n   int agentMask;\n   transient float watchProb;\n@@ -1372,9 +1921,5 @@ public class Agent extends DBObject implements Favorite\n   private Matcher[] keywordMatchers;\n   private String cachedKeywordForMats;\n   private transient Wizard wiz;\n-  private transient Airing[] relAirCache;\n-  private transient long cacheTimestamp;\n-  private transient Airing[] relAirCache2;\n-  private transient long cacheTimestamp2;\n   private transient boolean negator = false;\n }\ndiff --git a/java/sage/Airing.java b/java/sage/Airing.java\nindex 2e626f33..5676e420 100644\n--- a/java/sage/Airing.java\n+++ b/java/sage/Airing.java\n@@ -201,7 +201,13 @@ public class Airing extends DBObject implements Schedulable\n         || (s != null && ((time - s.originalAirDate) < 28 * Sage.MILLIS_PER_DAY));\n   }\n \n-  // For Favorites that are back-to-back, we don\'t apply any padding at that junction\n+  /**\n+   * Get adjusted the starting time for this airing.\n+   * <p/>\n+   * For Favorites that are back-to-back, we don\'t apply any padding at that junction.\n+   *\n+   * @return Adjusted starting time for this airing\n+   */\n   public long getSchedulingStart()\n   {\n     ManualRecord mr = Wizard.getInstance().getManualRecord(this);\n@@ -210,7 +216,9 @@ public class Airing extends DBObject implements Schedulable\n     Agent bond = Carny.getInstance().getCauseAgent(this);\n     if (bond == null || bond.startPad == 0)\n       return time;\n-    else if (Carny.getInstance().isLoveAir(this) && Sage.getBoolean("remove_padding_on_back_to_back_favorites", true))\n+    // For Favorites that are back-to-back, we don\'t apply any padding at that junction\n+    if (!SeekerSelector.USE_BETA_SEEKER && Carny.getInstance().isLoveAir(this) &&\n+      Sage.getBoolean("remove_padding_on_back_to_back_favorites", true))\n     {\n       return getAdjustedSchedulingStart(bond);\n     }\n@@ -254,6 +262,13 @@ public class Airing extends DBObject implements Schedulable\n     return time - bond.startPad;\n   }\n \n+  /**\n+   * Get adjusted the full duration of this airing.\n+   * <p/>\n+   * For Favorites that are back-to-back, we don\'t apply any padding at that junction.\n+   *\n+   * @return Adjusted full duration time of this airing\n+   */\n   public long getSchedulingDuration()\n   {\n     ManualRecord mr = Wizard.getInstance().getManualRecord(this);\n@@ -262,7 +277,7 @@ public class Airing extends DBObject implements Schedulable\n     Agent bond = Carny.getInstance().getCauseAgent(this);\n     if (bond == null || (bond.stopPad == 0 && bond.startPad == 0))\n       return duration;\n-    if (Carny.getInstance().isLoveAir(this) &&\n+    if (!SeekerSelector.USE_BETA_SEEKER && Carny.getInstance().isLoveAir(this) &&\n         Sage.getBoolean("remove_padding_on_back_to_back_favorites", true))\n     {\n       return getAdjustedSchedulingEnd(bond) - getAdjustedSchedulingStart(bond);\n@@ -270,6 +285,13 @@ public class Airing extends DBObject implements Schedulable\n     return duration + bond.stopPad + bond.startPad;\n   }\n \n+  /**\n+   * Get adjusted the ending time for this airing.\n+   * <p/>\n+   * For Favorites that are back-to-back, we don\'t apply any padding at that junction.\n+   *\n+   * @return Adjusted ending time for this airing\n+   */\n   public long getSchedulingEnd()\n   {\n     ManualRecord mr = Wizard.getInstance().getManualRecord(this);\n@@ -278,7 +300,8 @@ public class Airing extends DBObject implements Schedulable\n     Agent bond = Carny.getInstance().getCauseAgent(this);\n     if (bond == null || bond.stopPad == 0)\n       return time + duration;\n-    else if (Carny.getInstance().isLoveAir(this) && Sage.getBoolean("remove_padding_on_back_to_back_favorites", true))\n+    else if (!SeekerSelector.USE_BETA_SEEKER && Carny.getInstance().isLoveAir(this) &&\n+      Sage.getBoolean("remove_padding_on_back_to_back_favorites", true))\n     {\n       return getAdjustedSchedulingEnd(bond);\n     }\n@@ -338,6 +361,11 @@ public class Airing extends DBObject implements Schedulable\n     return (myShow != null) ? myShow : (myShow = Wizard.getInstance().getShowForID(showID));\n   }\n \n+  public int getShowID()\n+  {\n+    return showID;\n+  }\n+\n   public long getTTA() { return Math.max(0, time - Sage.time()); }\n \n   public Channel getChannel() { return Wizard.getInstance().getChannelForStationID(stationID); }\n@@ -572,7 +600,7 @@ public class Airing extends DBObject implements Schedulable\n   public int getTotalParts() { return partsB & 0x0F; }\n   public int getPartNum() { return (partsB >> 4) & 0x0F; }\n \n-  public void appendMiscInfo(StringBuffer sb)\n+  public void appendMiscInfo(StringBuilder sb)\n   {\n     boolean addComma = false;\n     if ((partsB & 0x0F) > 1)\n@@ -724,7 +752,7 @@ public class Airing extends DBObject implements Schedulable\n   }\n   public String getMiscInfo()\n   {\n-    StringBuffer sb = new StringBuffer();\n+    StringBuilder sb = new StringBuilder();\n     appendMiscInfo(sb);\n     return sb.toString();\n   }\n@@ -1025,6 +1053,10 @@ public class Airing extends DBObject implements Schedulable\n     }\n   };\n \n+  /**\n+   * If start times are the same, sort by station ID. If the start times are different, sort by\n+   * start time. Sorting is low to high.\n+   */\n   public static final Comparator<Airing> TIME_CHANNEL_COMPARATOR =\n       new Comparator<Airing>()\n   {\n@@ -1043,6 +1075,10 @@ public class Airing extends DBObject implements Schedulable\n     }\n   };\n \n+  /**\n+   * If station ID\'s are the same, the sort by start time. If station ID\'s are different, sort by\n+   * station ID. Sorting is low to high.\n+   */\n   public static final Comparator<Airing> CHANNEL_TIME_COMPARATOR =\n       new Comparator<Airing>()\n   {\ndiff --git a/java/sage/BigBrother.java b/java/sage/BigBrother.java\nindex b6047743..61d3e93d 100644\n--- a/java/sage/BigBrother.java\n+++ b/java/sage/BigBrother.java\n@@ -168,7 +168,7 @@ public class BigBrother\n   {\n     sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.WATCHED_STATE_CHANGED,\n         new Object[] { sage.plugin.PluginEventManager.VAR_AIRING, a });\n-    Scheduler.getInstance().kick(true);\n+    SchedulerSelector.getInstance().kick(true);\n   }\n   public static void clearWatched(Airing a)\n   {\n@@ -304,18 +304,23 @@ public class BigBrother\n   }\n \n   public static boolean areSameShow(Airing a1, Airing a2, boolean matchFavs)\n+  {\n+    return areSameShow(a1, a2, matchFavs, null);\n+  }\n+\n+  public static boolean areSameShow(Airing a1, Airing a2, boolean matchFavs, StringBuilder sbCache)\n   {\n     if (a1 != null && a2 != null && a1.getShow() != null && a1.showID == a2.showID)\n     {\n       if ((isUnique(a1) || a1.time == a2.time) && (!matchFavs ||\n-          Carny.getInstance().areSameFavorite(a1, a2)))\n+          Carny.getInstance().areSameFavorite(a1, a2, sbCache)))\n         return true;\n       // Check for the Eastern/Pacific channel difference\n       Channel c1 = a1.getChannel();\n       Channel c2 = a2.getChannel();\n       if ((c1 != null && c2 != null && ((c1.name.length() > 1 && c1.name.charAt(c1.name.length() - 1) == \'P\') ||\n           (c2.name.length() > 1 && c2.name.charAt(c2.name.length() - 1) == \'P\')) && Math.abs(a1.time - a2.time) == 3*Sage.MILLIS_PER_HR) &&\n-          (!matchFavs || Carny.getInstance().areSameFavorite(a1, a2)))\n+          (!matchFavs || Carny.getInstance().areSameFavorite(a1, a2, sbCache)))\n         return true;\n     }\n     return false;\n@@ -391,9 +396,7 @@ public class BigBrother\n         s.cachedUnique = Show.PRESUMED_UNIQUE; return true;\n       }\n \n-      if ("movie".equalsIgnoreCase(s.getCategory()) ||\n-          "sports".equalsIgnoreCase(s.getCategory()) ||\n-          s.hasEpisodeName())\n+      if (s.isCategory("movie") || s.isCategory("sports") || s.hasEpisodeName())\n       {\n         s.cachedUnique = Show.PRESUMED_UNIQUE;\n         return true;\n@@ -442,9 +445,7 @@ public class BigBrother\n         return true;\n       }\n \n-      if ("movie".equalsIgnoreCase(s.getCategory()) ||\n-          "sports".equalsIgnoreCase(s.getCategory()) ||\n-          s.hasEpisodeName())\n+      if (s.isCategory("movie") || s.isCategory("sports") || s.hasEpisodeName())\n       {\n         return true;\n       }\ndiff --git a/java/sage/Carny.java b/java/sage/Carny.java\nindex cace5384..fc2d0461 100644\n--- a/java/sage/Carny.java\n+++ b/java/sage/Carny.java\n@@ -15,7 +15,13 @@\n  */\n package sage;\n \n+import sage.util.MutableFloat;\n+import sage.util.MutableInteger;\n+\n import java.io.File;\n+import java.io.FileOutputStream;\n+import java.io.IOException;\n+import java.io.PrintWriter;\n import java.util.ArrayList;\n import java.util.Arrays;\n import java.util.Collections;\n@@ -26,11 +32,30 @@ import java.util.List;\n import java.util.Map;\n import java.util.Set;\n import java.util.Vector;\n+import java.util.concurrent.Callable;\n+import java.util.concurrent.ConcurrentLinkedQueue;\n+import java.util.concurrent.ExecutorService;\n+import java.util.concurrent.Executors;\n+import java.util.concurrent.Future;\n+import java.util.concurrent.ThreadFactory;\n+import java.util.concurrent.TimeUnit;\n+import java.util.concurrent.TimeoutException;\n \n public final class Carny implements Runnable\n {\n   private static final String GLOBAL_WATCH_COUNT = "global_watch_count";\n   private static final String LIMITED_CARNY_INIT = "limited_carny_init";\n+  // The user can define exactly how many threads will be used. This is not checked against the\n+  // total core count, so the user could put in a number that\'s more if they want to experiment.\n+  // There is no evidence that that would have a positive effect.\n+  private static final String LIMITED_CARNY_THREADS = "limited_carny_threads";\n+  // This disables the mapping optimizations in favor of lower memory usage. In my own measurements\n+  // with about 64k airings and 12k agents, the maps consumed about 2MB. (JS)\n+  private static final String DISABLE_CARNY_MAPS = "disable_carny_maps";\n+  // This performs a bit shift of the provided value to the hashes in the map. The goal is to\n+  // increase collisions which will cause more airings to be grouped together at the expense of a\n+  // very moderate performance loss.\n+  private static final String MAP_DENSITY = "carny_map_density";\n   static final String CARNY_KEY = "carny";\n \n   private static final long LOOKAHEAD = Sage.EMBEDDED ? 10*24*60*60*1000L : 14*24*60*60*1000L;\n@@ -40,6 +65,8 @@ public final class Carny implements Runnable\n    */\n   private static final float MIN_WP = 0;//1e-6f;\n   public static final long SLEEP_PERIOD = Sage.EMBEDDED ? 120 : 30;\n+  // The processor count is used to limit the number of threads we will spin up for agent processing.\n+  public static final int PROCESSOR_COUNT = Math.max(Runtime.getRuntime().availableProcessors(), 1);\n \n   public static final Object WATCH_MARK_JOB = new Object();\n   public static final Object WATCH_REAL_JOB = new Object();\n@@ -103,6 +130,17 @@ public final class Carny implements Runnable\n     mustSeeSet = Collections.synchronizedSet(new HashSet<Airing>());\n     loveAirSet = Collections.synchronizedSet(new HashSet<Airing>());\n     swapMap = new HashMap<Airing, Airing>();\n+    agentWorkQueue = new ConcurrentLinkedQueue<>();\n+    agentWorkers = Executors.newCachedThreadPool(new ThreadFactory()\n+    {\n+      @Override\n+      public Thread newThread(Runnable r)\n+      {\n+        Thread newThread = new Thread(r);\n+        newThread.setName("CarnyAgentWorker");\n+        return newThread;\n+      }\n+    });\n     prepped = false;\n   }\n \n@@ -125,7 +163,7 @@ public final class Carny implements Runnable\n     if (!doneInit)\n     {\n       lengthyInit(false);\n-      Scheduler.getInstance().kick(false);\n+      SchedulerSelector.getInstance().kick(false);\n     }\n     while (alive)\n     {\n@@ -170,6 +208,9 @@ public final class Carny implements Runnable\n     {\n       jobs.notifyAll();\n     }\n+    // alive being changed to false will be enough to cause a graceful termination of the agent\n+    // worker threads.\n+    agentWorkers.shutdown();\n   }\n \n   // Any non-standard processing we should do immediately, profiling calculations\n@@ -300,7 +341,7 @@ public final class Carny implements Runnable\n           + start + ". added " + airsToAdd.size() + " shows.");\n     } else {\n       DBObject[] rawAirs = wiz.getRawAccess(Wizard.AIRING_CODE, Wizard.AIRINGS_BY_CT_CODE);\n-      StringBuffer sbCache = new StringBuffer();\n+      StringBuilder sbCache = new StringBuilder();\n       for (int i = 0; i < rawAirs.length; i++)\n       {\n         Airing a = (Airing) rawAirs[i];\n@@ -354,7 +395,7 @@ public final class Carny implements Runnable\n         pots = tempSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n       }\n       clientSyncAll();\n-      Scheduler.getInstance().kick(true);\n+      SchedulerSelector.getInstance().kick(true);\n     }\n   }\n \n@@ -433,7 +474,7 @@ public final class Carny implements Runnable\n       airs = wiz.getRawAccess(Wizard.AIRING_CODE, Wizard.AIRINGS_BY_CT_CODE);\n     }\n \n-    StringBuffer sbCache = new StringBuffer();\n+    StringBuilder sbCache = new StringBuilder();\n     for (int i = 0; i < airs.length; i++)\n     {\n       Airing a = (Airing) airs[i];\n@@ -494,7 +535,7 @@ public final class Carny implements Runnable\n               a.time >= Sage.time() - Scheduler.SCHEDULING_LOOKBEHIND &&\n               a.stationID != 0)\n           {\n-            wpMap.put(a, new Float(1.0f));\n+            wpMap.put(a, 1.0f);\n             if (!dontScheduleThisAgent)\n             {\n               mustSeeSet.add(a);\n@@ -512,7 +553,7 @@ public final class Carny implements Runnable\n          * retained. Then we\'ll clean that up on the next Carny round. Better than letting them drop off into\n          * oblivion temporarily and risk a delete cycle kicking in that destroys the content.\n          */\n-        Float oldFavWP = new Float(0.9f);\n+        Float oldFavWP = 0.9f;\n         synchronized (wpMap)\n         {\n           for (Airing air : airsThatMayDie) {\n@@ -528,7 +569,7 @@ public final class Carny implements Runnable\n         pots = tempSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n       }\n       clientSyncAll();\n-      Scheduler.getInstance().kick(true);\n+      SchedulerSelector.getInstance().kick(true);\n     }\n     submitJob(new Object[] { LOVE_JOB, null });\n     sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.FAVORITE_MODIFIED,\n@@ -585,7 +626,7 @@ public final class Carny implements Runnable\n     } else {\n       airs = wiz.getRawAccess(Wizard.AIRING_CODE, Wizard.AIRINGS_BY_CT_CODE);\n     }\n-    StringBuffer sbCache = new StringBuffer();\n+    StringBuilder sbCache = new StringBuilder();\n     boolean resyncAll = false;\n     for (int i = 0; i < airs.length; i++)\n     {\n@@ -637,12 +678,12 @@ public final class Carny implements Runnable\n         pots = tempSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n       }\n       clientSyncAll();\n-      Scheduler.getInstance().kick(true);\n+      SchedulerSelector.getInstance().kick(true);\n     }\n     else if (resyncAll)\n     {\n       clientSyncAll();\n-      Scheduler.getInstance().kick(true);\n+      SchedulerSelector.getInstance().kick(true);\n     }\n \n     clientSyncLoves();\n@@ -726,12 +767,12 @@ public final class Carny implements Runnable\n   public void addDontLike(Airing air, boolean manual)\n   {\n     submitWasteJob(air, true, manual);\n-    Scheduler.getInstance().kick(true);\n+    SchedulerSelector.getInstance().kick(true);\n   }\n   public void removeDontLike(Airing air)\n   {\n     submitWasteJob(air, false, true);\n-    Scheduler.getInstance().kick(true);\n+    SchedulerSelector.getInstance().kick(true);\n   }\n   private void submitWasteJob(Airing air, boolean doWaste, boolean manual)\n   {\n@@ -907,7 +948,7 @@ public final class Carny implements Runnable\n     if (s == null || !wasteAir.isTV() || Sage.EMBEDDED) return;\n     Stringer stit = s.title;\n     Agent[] allAgents = wiz.getAgents();\n-    StringBuffer sbCache = new StringBuffer();\n+    StringBuilder sbCache = new StringBuilder();\n     for (int i = 0; i < allAgents.length; i++)\n     {\n       if (allAgents[i] == null) continue;\n@@ -926,25 +967,182 @@ public final class Carny implements Runnable\n         0, null, null);\n   }\n \n+  private void addToMaps(Integer hash, Airing airing, Map<Integer, List<Airing>> buildMap)\n+  {\n+    // We did try to limit the number of hashes we will actually map based on what the agents say\n+    // they will use, but what ends up happening is a race condition leading to an agent that can\'t\n+    // find an airing that it should have been able to find if we had not already filtered\n+    // everything, so we map everything regardless of if we know it will be used.\n+    List<Airing> mappedAirings = buildMap.get(hash);\n+    // We are using an array list to keep things in order so that we can do a binary search later.\n+    // Most of the data with the exception of airings sorted by channel/time should already be\n+    // sorted, and we will do a final sort on that one specifically.\n+    if (mappedAirings == null)\n+    {\n+      // 20 is selected based on random data samples. Often we only have one to two airings per\n+      // array which can make this a really inefficient selection, but more often, we are close to\n+      // 16 airings. This is admittedly a super small optimization, but we know we are going to put\n+      // at least one airing in here, so creating the array is always going to happen at least once\n+      // and this might keep us from needing to create a new one to expand.\n+      mappedAirings = new ArrayList<>(20);\n+      buildMap.put(hash, mappedAirings);\n+      mappedAirings.add(airing);\n+    }\n+    else\n+    {\n+      // If we might be adding a duplicate, it will be the last entry because we call this multiple\n+      // times for the same airing and it might have redundant hashes.\n+      if (mappedAirings.get(mappedAirings.size() - 1) != airing)\n+      {\n+        mappedAirings.add(airing);\n+      }\n+    }\n+  }\n+\n+  // Re-build the airing map for wasted so we don\'t need to keep re-getting wasted from the wizard.\n+  // Wasted will cache it\'s airing on the first get which also keeps the requests at a minimum.\n+  // This also keeps the airing binary sort which is desirable.\n+  private Map<Integer, Wasted[]> buildWastedMap(Airing[] airings, int bitShift)\n+  {\n+    Map<Integer, Airing[]> airsMap = buildMap(airings, bitShift);\n+    Map<Integer, Wasted[]> wastedMap = new HashMap<>((int)(airsMap.size() / 0.75) + 1);\n+\n+    for (Map.Entry<Integer, Airing[]>  entry : airsMap.entrySet())\n+    {\n+      Airing airs[] = entry.getValue();\n+      Wasted wasted[] = new Wasted[airs.length];\n+      int w = 0;\n+      for (int i = 0; i < airs.length; i++)\n+      {\n+        Wasted currWasted = wiz.getWastedForAiring(airs[i]);\n+        if (currWasted != null)\n+          wasted[w++] = currWasted;\n+      }\n+      // Somehow we lost one while re-building.\n+      if (w != airs.length)\n+        wasted = Arrays.copyOf(wasted, w);\n+      wastedMap.put(entry.getKey(), wasted);\n+    }\n+    return wastedMap;\n+  }\n+\n+  private Map<Integer, Airing[]> buildMap(Airing airings[], int bitShift)\n+  {\n+    Map<Integer, List<Airing>> buildMap = new HashMap<>();\n+    for (int i = 0, airingsSize = airings.length; i < airingsSize; i++)\n+    {\n+      Airing airing = airings[i];\n+      Show show = airing.getShow();\n+      if (show == null)\n+        continue;\n+\n+      int currentHash = show.title == null ? 0 : show.title.ignoreCaseHash;\n+      addToMaps((currentHash >>> bitShift), airing, buildMap);\n+\n+      Person[] people = show.people;\n+      for (int j = 0, peopleLength = people.length; j < peopleLength; j++)\n+      {\n+        Person person = people[j];\n+        currentHash = person.ignoreCaseHash;\n+        addToMaps((currentHash >>> bitShift), airing, buildMap);\n+      }\n+\n+      if (show.categories != null)\n+      {\n+        Stringer[] categories = show.categories;\n+        // We don\'t do anything with more than the first and second category entries.\n+        for (int j = 0, categoriesLength = Math.min(2, categories.length); j < categoriesLength; j++)\n+        {\n+          Stringer category = categories[j];\n+          currentHash = category.ignoreCaseHash;\n+          addToMaps((currentHash >>> bitShift), airing, buildMap);\n+        }\n+      }\n+\n+      Channel c = airing.getChannel();\n+      if (c != null)\n+      {\n+        // Sometimes there\'s a channel, but it doesn\'t have a name. We never match for channels\n+        // that do not have a name and this ends up being a zero forcing all of the agents to\n+        // process it even though there isn\'t a reason to do so.\n+        if (c.name != null && c.name.length() > 0)\n+        {\n+          currentHash = c.name.hashCode();\n+          addToMaps((currentHash >>> bitShift), airing, buildMap);\n+        }\n+\n+        // Same situation with network (even more so).\n+        if (c.network != null && c.network.name.length() > 0)\n+        {\n+          currentHash = c.network.ignoreCaseHash;\n+          addToMaps((currentHash >>> bitShift), airing, buildMap);\n+        }\n+      }\n+\n+      if (show.rated != null)\n+      {\n+        currentHash = show.rated.ignoreCaseHash;\n+        addToMaps((currentHash >>> bitShift), airing, buildMap);\n+      }\n+\n+      if (show.year != null)\n+      {\n+        currentHash = show.year.ignoreCaseHash;\n+        addToMaps((currentHash >>> bitShift), airing, buildMap);\n+      }\n+\n+      if (show.pr != null)\n+      {\n+        currentHash = show.pr.ignoreCaseHash;\n+        addToMaps((currentHash >>> bitShift), airing, buildMap);\n+      }\n+\n+      // Agents that use the slotType and timeslots without any other criteria need to process all\n+      // airs because there isn\'t currently an efficient way to map that data for quick lookups.\n+\n+      // Agents that use keywords without any other criteria need to process all airs because we\n+      // have not come up with and efficient way to look them up.\n+    }\n+\n+    // We could have this array around for a long time, so let\'s make sure we have the smallest\n+    // possible footprint. Also arrays are just faster than lists to iterate.\n+    Map<Integer, Airing[]> returnMap = new HashMap<>(Math.max (16, (int)(buildMap.size() / 0.75f) + 1));\n+\n+    for (Map.Entry<Integer, List<Airing>> entry : buildMap.entrySet())\n+    {\n+      Airing[] finalAirings = entry.getValue().toArray(Pooler.EMPTY_AIRING_ARRAY);\n+      returnMap.put(entry.getKey(), finalAirings);\n+    }\n+    return returnMap;\n+  }\n+\n   private void stdProcessing()\n   {\n+    String lastMessage = null;\n     if (!doneInit)\n     {\n-      Sage.setSplashText(Sage.rez("Module_Init_Progress", new Object[] { Sage.rez("Profiler"), new Double(0)}));\n+      lastMessage = Sage.rez("Module_Init_Progress", new Object[] { Sage.rez("Profiler"), new Double(0)});\n+      Sage.setSplashText(lastMessage);\n     }\n-    // This tracks when we started trying to compile the profiler info so we don\'t keep aborting attempts forever if it takes\n-    // a long time to figure out\n+\n+    boolean disableCarnyMaps = Sage.getBoolean(DISABLE_CARNY_MAPS, false);\n+    // Windows is still 32-bit, so we use this to keep memory usage down by what can be a\n+    // substantial amount.\n+    // 05/20/2017 JS: I did have 1 bit shifted for Linux, but I felt might not work well for\n+    // everyone, so I changed it to 0.\n+    int mapBitShift = Sage.getInt(MAP_DENSITY, Sage.WINDOWS_OS ? 16 : 0);\n+    // Don\'t let users push this beyond what even makes sense. Always leave at least 9 bits or we\n+    // should just turn the maps off.\n+    if (mapBitShift >= 28)\n+      mapBitShift = 27;\n+    else if (mapBitShift < 0)\n+      mapBitShift = 0;\n+    // This tracks when we started trying to compile the profiler info so we don\'t keep aborting\n+    // attempts forever if it takes a long time to figure out\n     if (lastCycleCompleteTime >= cycleStartTime)\n       cycleStartTime = Sage.eventTime();\n     // Go through each Agent and run their think() process.\n-    Map<Airing, Float> newWPMap = Collections.synchronizedMap(new HashMap<Airing, Float>());\n-    Map<Airing, Agent> newCauseMap = Collections.synchronizedMap(new HashMap<Airing, Agent>());\n-    Set<Airing> airSet = new HashSet<Airing>();\n-    Set<Airing> newMustSeeSet = Collections.synchronizedSet(new HashSet<Airing>());\n-    Set<Airing> blackBalled = new HashSet<Airing>();\n-\n-    DBObject[] allAgents = wiz.getRawAccess(Wizard.AGENT_CODE, (byte) 0);\n-    Set<Airing> newLoveAirSet = Collections.synchronizedSet(new HashSet<Airing>());\n+    DBObject[] allAgents = wiz.getRawAccess(Wizard.AGENT_CODE, Wizard.AGENTS_BY_CARNY_CODE);\n \n     // We don\'t track music at all (that used to be in Agent.followsTrend)\n \n@@ -953,190 +1151,424 @@ public final class Carny implements Runnable\n       // Clear this out since we\'re getting fresh airings from the DB now\n       swapMap.clear();\n     }\n-    DBObject[] rawAirs = wiz.getRawAccess(Wizard.AIRING_CODE,\n-        Wizard.AIRINGS_BY_CT_CODE);\n-    Set<Airing> airset = new HashSet<Airing>();\n-    List<Airing> remAirSet = new ArrayList<Airing>();\n+    // We are getting binary sorted by CT code because these will not include old airings that have\n+    // been replaced with new ones on the same station and timeslot.\n+    DBObject[] rawAirs = wiz.getRawAccess(Wizard.AIRING_CODE, Wizard.AIRINGS_BY_CT_CODE);\n+    // This seems to split the airings in half, but not perfectly, so we allocate 65% of the total\n+    // to each array. Also unless we are incorrectly populating the database, we have no reason to\n+    // use a set for either of these. All airings will be unique objects and using a set would\n+    // create thousands of new objects for no gain and slower overall insertion performance due to\n+    // the objects created for every new entry.\n+    List<Airing> airset = new ArrayList<>(Math.max(10, (int)(rawAirs.length * 0.65f)));\n+    List<Airing> remAirSet = new ArrayList<>(Math.max(10, (int)(rawAirs.length * 0.65f)));\n+\n     long currLookahead = Sage.getLong("scheduling_lookahead", LOOKAHEAD);\n     int testMask = DBObject.MEDIA_MASK_TV;\n     long currTime = Sage.time();\n-    for (int i = 0; i < rawAirs.length; i++)\n+    for (int i = 0, rawAirsLen = rawAirs.length; i < rawAirsLen; i++)\n     {\n       Airing a = (Airing) rawAirs[i];\n       // Only use TV Airings in this calculation\n       if (a == null || !a.hasMediaMaskAny(testMask)) continue;\n-      if (a.time < currTime + currLookahead &&\n-          a.time >= currTime - Scheduler.SCHEDULING_LOOKBEHIND && a.isTV())\n+      if (a.getStartTime() < currTime + currLookahead &&\n+          a.getStartTime() >= currTime - Scheduler.SCHEDULING_LOOKBEHIND && a.isTV())\n+      {\n         airset.add(a);\n+      }\n       else\n+      {\n         remAirSet.add(a);\n+      }\n     }\n+    // We need to sort for the media file insertion step so that we can do a binary search.\n+    Collections.sort(airset, DBObject.ID_COMPARATOR);\n+    // We need to sort this one for later so we don\'t need to sort 1000\'s of smaller arrays.\n+    Collections.sort(remAirSet, DBObject.ID_COMPARATOR);\n+\n     // We also need to be sure we analyze all of the files\n     MediaFile[] mfs = wiz.getFiles();\n-    for (int i = 0; i < mfs.length; i++)\n+    for (int i = 0, mfsLen = mfs.length; i < mfsLen; i++)\n     {\n-      if (!mfs[i].archive && mfs[i].isTV())\n-        airset.add(mfs[i].getContentAiring());\n+      MediaFile mf = mfs[i];\n+      if (mf != null && !mf.isArchiveFile() && mf.isTV())\n+      {\n+        Airing contentAir = mf.getContentAiring();\n+        // Ensure we are inserting unique airings since we are not using a set.\n+        int index = Collections.binarySearch(airset, contentAir, DBObject.ID_COMPARATOR);\n+        if (index < 0)\n+        {\n+          index = -(index + 1);\n+          airset.add(index, contentAir);\n+        }\n+      }\n     }\n \n-    Airing[] allAirs = airset.toArray(Pooler.EMPTY_AIRING_ARRAY);\n-    Airing[] remAirs = remAirSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n-\n-    List<Agent> traitors = new ArrayList<Agent>();\n-    allAgents = wiz.getAgents();\n-    Set<Airing> watchedPotsToClear = new HashSet<Airing>();\n-    if (Sage.DBG) System.out.println("CARNY Processing " + allAgents.length + " Agents & " + allAirs.length + " Airs");\n-    boolean controlCPUUsage = doneInit && allAgents.length > 50 && Sage.getBoolean("control_profiler_cpu_usage", true);\n-    // This array is used in the the Agent\'s calcWatchProb to avoid having to re-allocate a storage location each time we go through it.\n-    // We make it bigger than what could possibly come up in the Agent\'s calc (i.e. every watch & waste matching it and none overlapping)\n-    Airing[] airWorkCache = new Airing[1000 + wiz.getRawAccess(Wizard.WASTED_CODE, (byte) 0).length + wiz.getRawAccess(Wizard.WATCH_CODE, (byte) 0).length];\n-    String paidProgRez = Sage.rez("Paid_Programming");\n-    // We do a lot of String work for keyword favorites so have a buffer that they can do\n-    // that work in to massively reduce GC\'d memory allocations\n-    StringBuffer sbCache = new StringBuffer();\n-    String lastSplashMsg = null;\n-    for (int i = 0; i < allAgents.length; i++)\n+    // Add all of the work into a shared queue so we don\'t end up giving one thread a lot more work\n+    // over another.\n+    int submittedAgents;\n+    if (!doneInit && Sage.getBoolean(LIMITED_CARNY_INIT, true))\n     {\n-      if (!doneInit)\n+      for (int i = 0, allAgentsLen = allAgents.length; i < allAgentsLen; i++)\n       {\n-        String newSplashMsg = Sage.rez("Module_Init_Progress", new Object[] { Sage.rez("Profiler"), new Double((i*1.0)/allAgents.length)});\n-        if (!newSplashMsg.equals(lastSplashMsg))\n+        Agent currAgent = (Agent) allAgents[i];\n+        // This is an inexpensive check against two flags so we aren\'t adding agents to the queue\n+        // that we already know shouldn\'t be there.\n+        if (currAgent != null && !currAgent.testAgentFlag(Agent.DISABLED_FLAG) && currAgent.isFavorite())\n         {\n-          Sage.setSplashText(newSplashMsg);\n-          lastSplashMsg = newSplashMsg;\n+          agentWorkQueue.add(currAgent);\n         }\n       }\n-      if (controlCPUUsage)\n-        try{Thread.sleep(SLEEP_PERIOD);}catch(Exception e){}\n-      if (doneInit)\n+      submittedAgents = agentWorkQueue.size();\n+    }\n+    else\n+    {\n+      for (int i = 0, allAgentsLen = allAgents.length; i < allAgentsLen; i++)\n       {\n-        // Check to see if something else is a higher priority to calculate.\n-        // NOTE: If we\'ve been trying to build the profile for a half hour and haven\'t finished then it\'s\n-        // likely that stopping early could cause us to never finish since recordings end/start so often.\n-        // So in that case we do NOT stop processing early and just continue on our merry way!\n-        if (!shouldContinueStdProcessing(Sage.eventTime() - cycleStartTime < 30*Sage.MILLIS_PER_MIN))\n+        Agent currAgent = (Agent) allAgents[i];\n+        // This is an inexpensive check against a flag so we aren\'t adding agents to the queue that\n+        // we already know shouldn\'t be there.\n+        if (currAgent != null && !currAgent.testAgentFlag(Agent.DISABLED_FLAG))\n         {\n-          if (Sage.DBG) System.out.println("Carny is stopping processing early to do a new job...");\n-          return; // just bail, it\'ll pick it all up afterwards\n+          agentWorkQueue.add(currAgent);\n         }\n       }\n-      Agent currAgent = (Agent) allAgents[i];\n-      if (currAgent == null || currAgent.testAgentFlag(Agent.DISABLED_FLAG))\n-        continue;\n+      submittedAgents = agentWorkQueue.size();\n+    }\n \n-      if ((!doneInit && Sage.getBoolean(LIMITED_CARNY_INIT, true)) ||\n-          (Sage.EMBEDDED && Seeker.getInstance().getDisableProfilerRecording()))\n-      {\n-        if (!currAgent.isFavorite())\n-          continue;\n-      }\n+    Map<Integer, Airing[]> allAirsMap;\n+    Map<Integer, Airing[]> remAirsMap;\n+    Map<Integer, Airing[]> watchAirsMap;\n+    Map<Integer, Wasted[]> wastedAirsMap;\n \n-      if (!currAgent.calcWatchProb(controlCPUUsage, airWorkCache, sbCache))\n-      {\n-        traitors.add(currAgent);\n-        continue;\n-      }\n-      Airing[] agePots = currAgent.getRelatedAirings(allAirs, true, controlCPUUsage, sbCache);\n-      if (currAgent.isFavorite())\n-      {\n-        newLoveAirSet.addAll(Arrays.asList(agePots));\n+    Airing[] allAirs;\n+    Airing[] remAirs;\n+    Airing[] watchAirs;\n+    Airing[] wastedAirs;\n \n-        // Also check the rem airs so we\'re sure we get EVERYTHING in the DB\n-        // that applies to this Favorite included in the group.\n-        Airing[] remRelated = currAgent.getRelatedAirings(remAirs, false, controlCPUUsage, sbCache);\n-        newLoveAirSet.addAll(Arrays.asList(remRelated));\n-      }\n+    if (disableCarnyMaps)\n+    {\n+      allAirsMap = null;\n+      remAirsMap = null;\n+      watchAirsMap = null;\n+      wastedAirsMap = null;\n+\n+      allAirs = airset.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+      remAirs = remAirSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+      watchAirs = null;\n+      wastedAirs = null;\n+    }\n+    else\n+    {\n+      System.out.println("CARNY building airing maps...");\n \n-      // Check to see if this Agent is a Favorite who has a keep at most limit set with\n-      // manual deleting. In that case, we don\'t add any of its future airings to the\n-      // mustSeeSet or to the pots, but we do put them in the loveAirSet.\n-      boolean dontScheduleThisAgent = false;\n-      if (currAgent.isFavorite() && currAgent.testAgentFlag(Agent.DONT_AUTODELETE_FLAG) &&\n-          currAgent.getAgentFlag(Agent.KEEP_AT_MOST_MASK) > 0)\n+      DBObject[] watches = wiz.getRawAccess(Wizard.WATCH_CODE, (byte) 0);\n+      List<Airing> fullWatchAirsList = new ArrayList<>(watches.length);\n+      for (int i = 0, watchesLen = watches.length; i < watchesLen; i++)\n       {\n-        int fileCount = 0;\n-        for (int j = 0; j < agePots.length; j++)\n+        Watched currWatch = (Watched) watches[i];\n+        if (currWatch != null && BigBrother.isFullWatch(currWatch))\n         {\n-          MediaFile mf = wiz.getFileForAiring(agePots[j]);\n-          if (mf != null && mf.isCompleteRecording())\n-            fileCount++;\n+          fullWatchAirsList.add(currWatch.getAiring());\n         }\n-        int keepAtMost = currAgent.getAgentFlag(Agent.KEEP_AT_MOST_MASK);\n-        if (fileCount >= keepAtMost)\n+      }\n+\n+      DBObject[] waste = wiz.getRawAccess(Wizard.WASTED_CODE, (byte) 0);\n+      List<Airing> wastedAirsList = new ArrayList<>(waste.length);\n+      for (int i = 0, wasteLen = waste.length; i < wasteLen; i++)\n+      {\n+        Wasted currWaste = (Wasted) waste[i];\n+        if (currWaste != null)\n         {\n-          dontScheduleThisAgent = true;\n+          wastedAirsList.add(currWaste.getAiring());\n         }\n       }\n \n-      boolean negator = currAgent.isNegativeNelly();\n-      for (int j = 0; j < agePots.length; j++)\n+      // We need to sort these two now so we don\'t need to sort 1000\'s of smaller arrays.\n+      Collections.sort(fullWatchAirsList, DBObject.ID_COMPARATOR);\n+      Collections.sort(wastedAirsList, DBObject.ID_COMPARATOR);\n+\n+      allAirs = airset.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+      remAirs = remAirSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+      watchAirs = fullWatchAirsList.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+      wastedAirs = wastedAirsList.toArray(Pooler.EMPTY_AIRING_ARRAY);\n+\n+      allAirsMap = buildMap(allAirs, mapBitShift);\n+      remAirsMap = buildMap(remAirs, mapBitShift);\n+      watchAirsMap = buildMap(watchAirs, mapBitShift);\n+      wastedAirsMap = buildWastedMap(wastedAirs, mapBitShift);\n+    }\n+\n+    // Free these fairly huge arrays up for GC.\n+    airset = null;\n+    remAirSet = null;\n+    CarnyCache seedCache = new CarnyCache(allAirsMap, remAirsMap, watchAirsMap, wastedAirsMap);\n+\n+    if (Sage.DBG) System.out.println("CARNY Processing " + allAgents.length + " Agents & " + allAirs.length + " Airs");\n+\n+    boolean controlCPUUsage = doneInit && submittedAgents > 50 && Sage.getBoolean("control_profiler_cpu_usage", true);\n+    boolean useLegacyKeyword = Sage.getBoolean("use_legacy_keyword_favorites", true);\n+    boolean aggressiveNegativeProfiling = Sage.getBoolean("aggressive_negative_profiling", false);\n+\n+    // If we don\'t have any agents, there\'s no point in spinning up worker threads. Also some users\n+    // might have servers with a lot of cores (> 8), so if we don\'t have a lot of agents, we will\n+    // not try to spread the load across every core because it\'s just a waste of resources. We are\n+    // arbitrarily selecting 75 agents per core. The number could be higher, but on lower powered\n+    // machines this will make a more meaningful difference on startup.\n+    int totalThreads = Sage.getInt(LIMITED_CARNY_THREADS, 0);\n+    if (totalThreads <= 0)\n+    {\n+      totalThreads = submittedAgents == 0 ? 0 :\n+        Math.min(PROCESSOR_COUNT, Math.max(1, submittedAgents / 75));\n+    }\n+    // This is an unlikely number that will not break things like some crazy number someone might\n+    // set or an bad value returned for the processor count. There is known contention between the\n+    // threads since they all want to modify several common objects, so at some point there will be\n+    // diminishing returns. This could be an argument for concurrent objects, but nothing is\n+    // iterating the collections/maps while they are being modified and in real world testing the\n+    // contention does not cause any measurable performance loss.\n+    if (totalThreads > 64)\n+      totalThreads = 64;\n+    Future<CarnyWorkerCallback> agentWorkerFutures[] = new Future[totalThreads];\n+    // Start/resume all of the agent worker threads.\n+    for (int i = 0; i < totalThreads; i++)\n+    {\n+      Callable<CarnyWorkerCallback> newJob = new CarnyAgentWorker(controlCPUUsage, totalThreads,\n+        doneInit, useLegacyKeyword, aggressiveNegativeProfiling, mapBitShift, allAirs, remAirs,\n+        watchAirs, wastedAirs, i != 0 ? new CarnyCache(seedCache) : seedCache);\n+\n+      agentWorkerFutures[i] = agentWorkers.submit(newJob);\n+    }\n+\n+    int traitorsExpand = 0;\n+    int newLoveAirSetExpand = 0;\n+    int blackBalledExpand = 0;\n+    int airSetExpand = 0;\n+    int watchedPotsToClearExpand = 0;\n+    int newMustSeeSetExpand = 0;\n+    int newWPCausesExpand = 0;\n+\n+    boolean workCanceled = false;\n+    CarnyWorkerCallback firstCallback = null;\n+    int agentWorkerFuturesSize = agentWorkerFutures.length;\n+    int lastIndex = agentWorkerFuturesSize - 1;\n+    CarnyWorkerCallback callbacks[] = new CarnyWorkerCallback[agentWorkerFuturesSize];\n+    for (int i = 0;  i < agentWorkerFuturesSize; i++)\n+    {\n+      Future<CarnyWorkerCallback> future = agentWorkerFutures[i];\n+      try\n       {\n-        if (negator)\n+        CarnyWorkerCallback callback;\n+        boolean keepRunning;\n+        try\n         {\n-          blackBalled.add(agePots[j]);\n-          continue;\n-        }\n+          callback = doneInit ? future.get(5, TimeUnit.MINUTES) : future.get(250, TimeUnit.MILLISECONDS);\n+          keepRunning = callback.complete;\n \n-        // Skip this stuff\n-        if (paidProgRez.equalsIgnoreCase(agePots[j].getTitle()) ||\n-            wiz.isNoShow(agePots[j].showID))\n-          continue;\n+          if (keepRunning)\n+          {\n+            // Retain the callbacks for iteration later. We can optimize this further by not\n+            // actually merging everything here.\n+            callbacks[i] = callback;\n+            if (firstCallback == null)\n+            {\n+              firstCallback = callback;\n+            }\n \n-        airSet.add(agePots[j]);\n+            traitorsExpand += callback.traitors.size();\n+            newLoveAirSetExpand += callback.newLoveAirSet.size();\n+            blackBalledExpand += callback.blackBalled.size();\n+            airSetExpand += callback.airSet.size();\n+            watchedPotsToClearExpand += callback.watchedPotsToClear.size();\n+            newMustSeeSetExpand += callback.newMustSeeSet.size();\n+            newWPCausesExpand += callback.newWPCauses.size();\n+\n+            // On the last index, we expand the chosen callback arrays to accommodate the highest\n+            // number of agents/airings possible to be added.\n+            if (i == lastIndex)\n+            {\n+              List<Agent> destAgents = firstCallback.traitors;\n+              if (destAgents instanceof ArrayList)\n+                ((ArrayList) destAgents).ensureCapacity(traitorsExpand);\n \n-        /*\n-         * NOTE: On 7/22/03 I moved the isWatched() down from the beginning of this loop.\n-         * We want to keep watched airings in the Agent lookup table so we can\n-         * use the agentFlags on them even after they\'re watched. So now watched\n-         * airings will have a cause agent, but their WP will be zero.\n-         */\n-        /*\n-         * NOTE: Narflex - 8/4/08 - Fixed bug where we wouldn\'t use the higher priority favorite\n-         * in the cause map since comparing watchProb would be equal; this would then cause scheduling\n-         * issues because the wrong agent was used for the scheduling that should have been higher priority sometimes\n-         */\n+              List<Airing> destAirings = firstCallback.newLoveAirSet;\n+              if (destAirings instanceof ArrayList)\n+                ((ArrayList) destAirings).ensureCapacity(newLoveAirSetExpand);\n \n-        Float wp = newWPMap.get(agePots[j]);\n-        boolean replaceInMap = false;\n-        if (wp == null || (currAgent.watchProb > wp.floatValue()))\n-          replaceInMap = true;\n-        if (wp != null && currAgent.watchProb == wp.floatValue() && currAgent.isFavorite())\n+              destAirings = firstCallback.blackBalled;\n+              if (destAirings instanceof ArrayList)\n+                ((ArrayList) destAirings).ensureCapacity(blackBalledExpand);\n+\n+              destAirings = firstCallback.airSet;\n+              if (destAirings instanceof ArrayList)\n+                ((ArrayList) destAirings).ensureCapacity(airSetExpand);\n+\n+              destAirings = firstCallback.watchedPotsToClear;\n+              if (destAirings instanceof ArrayList)\n+                ((ArrayList) destAirings).ensureCapacity(watchedPotsToClearExpand);\n+\n+              destAirings = firstCallback.newMustSeeSet;\n+              if (destAirings instanceof ArrayList)\n+                ((ArrayList) destAirings).ensureCapacity(newMustSeeSetExpand);\n+\n+              List<WPCauseValue> destCauses = firstCallback.newWPCauses;\n+              if (destCauses instanceof ArrayList)\n+                ((ArrayList) destCauses).ensureCapacity(newWPCausesExpand);\n+\n+              for (int j = 0; j < agentWorkerFuturesSize; j++)\n+              {\n+                callback = callbacks[j];\n+                if (callback == firstCallback)\n+                  continue;\n+\n+                // Since we allowed the workers to build their own idea of watch potential and\n+                // causes, we need to consolidate those maps here. This is still faster because\n+                // the threads are not fighting each other over the same map and it removes a\n+                // synchronization requirement.\n+                List<WPCauseValue> srcCauses = callback.newWPCauses;\n+                for (int k = 0, size = srcCauses.size(); k < size; k++)\n+                {\n+                  WPCauseValue currCauseValue = srcCauses.get(k);\n+                  WPCauseValue mappedCauseValue = firstCallback.addOrReturnWPCauseValue(currCauseValue);\n+                  Airing agentPot = currCauseValue.airing;\n+\n+                  // We just added this agent.\n+                  if (mappedCauseValue == null)\n+                  {\n+                    if (agentPot.isWatchedForSchedulingPurpose())\n+                      firstCallback.addWatchedPotsToClear(agentPot);\n+                  }\n+                  // This must be run on all agents being merged or the sort order will be\n+                  // inconsistent between runs of Carny or completely incorrect.\n+                  else if (mappedCauseValue.compareAndReplace(currCauseValue.agent, true))\n+                  {\n+                    // When we passed true into the compare and replace method, it also replaced\n+                    // the agent if it has the same WP, but is logically not the agent we would have\n+                    // selected if this was all done on a single thread.\n+                    if (agentPot.isWatchedForSchedulingPurpose())\n+                      firstCallback.addWatchedPotsToClear(agentPot);\n+                  }\n+                }\n+\n+                List<Agent> srcAgents = callback.traitors;\n+                for (int k = 0, size = srcAgents.size(); k < size; k++)\n+                  firstCallback.addTraitor(srcAgents.get(k));\n+\n+                List<Airing> srcAirings = callback.newLoveAirSet;\n+                for (int k = 0, size = srcAirings.size(); k < size; k++)\n+                  firstCallback.addNewLoveAirSet(srcAirings.get(k));\n+\n+                srcAirings = callback.blackBalled;\n+                for (int k = 0, size = srcAirings.size(); k < size; k++)\n+                  firstCallback.addBlackBalled(srcAirings.get(k));\n+\n+                srcAirings = callback.airSet;\n+                for (int k = 0, size = srcAirings.size(); k < size; k++)\n+                  firstCallback.addAirSet(srcAirings.get(k));\n+\n+                srcAirings = callback.watchedPotsToClear;\n+                for (int k = 0, size = srcAirings.size(); k < size; k++)\n+                  firstCallback.addWatchedPotsToClear(srcAirings.get(k));\n+\n+                srcAirings = callback.newMustSeeSet;\n+                for (int k = 0, size = srcAirings.size(); k < size; k++)\n+                  firstCallback.addNewMustSeeSet(srcAirings.get(k));\n+              }\n+            }\n+          }\n+        }\n+        catch (TimeoutException e)\n         {\n-          // Check for agent priority\n-          Agent oldAgent = newCauseMap.get(agePots[j]);\n-          if (oldAgent == null || doBattle(oldAgent, currAgent) == currAgent)\n-            replaceInMap = true;\n+          // Do not proceed to the next future since we are only breaking out of waiting for it to\n+          // complete to update the logged progress. Otherwise we do not expect to ever find\n+          // ourselves in this block.\n+          i--;\n+          keepRunning = true;\n         }\n-        if (replaceInMap)\n+\n+        int jobsRemoved = submittedAgents - agentWorkQueue.size();\n+\n+        if (!doneInit)\n         {\n-          newCauseMap.put(agePots[j], currAgent);\n-          /*\n-           * We need to put the WP in temporarily even for watched stuff so the most powerful\n-           * Agent shows up in the map, otherwise a cat agent could override a favorite agent on a watched show\n-           */\n-          newWPMap.put(agePots[j], new Float(currAgent.watchProb));\n-          if (agePots[j].isWatchedForSchedulingPurpose())\n+          // Wait until we have actually finished the last worker thread before reporting 100%.\n+          if (jobsRemoved == submittedAgents && lastIndex != i)\n+            continue;\n+          // An empty database without any airings should go straight to 100%.\n+          String newSplashMsg = Sage.rez("Module_Init_Progress",\n+            new Object[]{Sage.rez("Profiler"), submittedAgents != 0 && allAirs.length != 0 ?\n+              new Double((jobsRemoved * 1.0) / submittedAgents) : new Double(1.0)});\n+          if (lastMessage == null || !newSplashMsg.equals(lastMessage))\n           {\n-            watchedPotsToClear.add(agePots[j]);\n+            Sage.setSplashText(newSplashMsg);\n           }\n+          lastMessage = newSplashMsg;\n         }\n-\n-        if (agePots[j].isWatchedForSchedulingPurpose())\n-          continue;\n-        if (dontScheduleThisAgent)\n+        else if (keepRunning && !workCanceled)\n         {\n-          MediaFile mf = wiz.getFileForAiring(agePots[j]);\n-          if (mf == null || !mf.isCompleteRecording())\n-          {\n-            watchedPotsToClear.add(agePots[j]);\n+          // Wait until we have actually finished the last worker thread before reporting 100%.\n+          if (jobsRemoved == submittedAgents && lastIndex != i)\n             continue;\n-          }\n+          // This isn\'t a completely accurate statement because removing an agent from the queue\n+          // doesn\'t mean we also finished processing the agent at the same time, but it is close\n+          // enough.\n+          if (Sage.DBG) System.out.println(\n+            "CARNY agent workers processed " + jobsRemoved + " of " + submittedAgents + " active agents.");\n+        }\n+\n+        // If a future returns false, that means we have a new job and need to stop all processing.\n+        // That means we will likely have excess jobs left in the queue that need to be cleared out.\n+        // It is ok to do this regardless of if other agent workers are still running because they\n+        // all need to stop processing anyway.\n+        if (!keepRunning)\n+        {\n+          System.out.println("CARNY agent worker returned canceled.");\n+          agentWorkQueue.clear();\n+          workCanceled = true;\n+        }\n+      }\n+      catch (Exception e)\n+      {\n+        // If we create any exceptions, everything is now invalid, so we need to remove the rest of\n+        // the agents. We still need to wait for all of the threads to return or we will create a\n+        // race condition and in this case that would be very bad.\n+        agentWorkQueue.clear();\n+        workCanceled = true;\n+        if (!(e instanceof InterruptedException) && Sage.DBG)\n+        {\n+          System.out.println("CARNY created an exception while processing: " + e.getMessage());\n+          e.printStackTrace(System.out);\n+          Throwable innerException = e.getCause();\n+          if (innerException != null)\n+            innerException.printStackTrace(System.out);\n         }\n-        if (currAgent.isFavorite())\n-          newMustSeeSet.add(agePots[j]);\n       }\n     }\n \n+    // If we have no agents to process, we will also get a null firstCallback without also getting\n+    // workCanceled.\n+    if (workCanceled || firstCallback == null)\n+      return;\n+\n+    final List<Agent> traitors = firstCallback.traitors;\n+    final Set<Airing> newLoveAirSet = new HashSet<>(firstCallback.newLoveAirSet);\n+    final List<Airing> blackBalled = firstCallback.blackBalled;\n+    final List<Airing> airSet = firstCallback.airSet;\n+    final List<Airing> watchedPotsToClear = firstCallback.watchedPotsToClear;\n+    final Set<Airing> newMustSeeSet = new HashSet<>(firstCallback.newMustSeeSet);\n+    final List<WPCauseValue> newWPCauses = firstCallback.newWPCauses;\n+\n+    int newWPCausesSize = newWPCauses.size();\n+    final Map<Airing, Float> newWPMap = new HashMap<>(Math.max(16, (int)(newWPCausesSize / 0.75f) + 1));\n+    final Map<Airing, Agent> newCauseMap = new HashMap<>(Math.max(16, (int)(newWPCausesSize / 0.75f) + 1));\n+\n+    for (int i = 0; i < newWPCausesSize; i++)\n+    {\n+      WPCauseValue value = newWPCauses.get(i);\n+      // Modifications to values are all synchronized on their own entry as they are changed by\n+      // multiple threads, so we need to do that here too.\n+      newWPMap.put(value.airing, value.wp);\n+      newCauseMap.put(value.airing, value.agent);\n+    }\n+\n     synchronized (this)\n     {\n       for (Airing oldAir : swapMap.keySet())\n@@ -1159,61 +1591,93 @@ public final class Carny implements Runnable\n     newWPMap.keySet().removeAll(watchedPotsToClear);\n \n     if (Sage.DBG) System.out.println("CARNY Negative Energy Size: " + (blackBalled.size() + stoners.length));\n-    for (Airing badAir : blackBalled)\n+    for (int i = 0, blackBalledSize = blackBalled.size(); i < blackBalledSize; i++)\n     {\n+      Airing badAir = blackBalled.get(i);\n       newWPMap.remove(badAir);\n       newCauseMap.remove(badAir);\n-      newMustSeeSet.remove(badAir);\n-      airSet.remove(badAir);\n+      // Replaced with binary removals.\n+      //newMustSeeSet.remove(badAir);\n+      firstCallback.removeNewMustSeeSet(badAir);\n+      //airSet.remove(badAir);\n+      firstCallback.removeAirSet(badAir);\n     }\n \n-    for (int i = 0; i < stoners.length; i++)\n+    for (int i = 0, stonersLen = stoners.length; i < stonersLen; i++)\n     {\n       Airing badAir = stoners[i].getAiring();\n       newWPMap.remove(badAir);\n       newCauseMap.remove(badAir);\n-      newMustSeeSet.remove(badAir);\n-      airSet.remove(badAir);\n+      // Replaced with binary removals.\n+      //newMustSeeSet.remove(badAir);\n+      firstCallback.removeNewMustSeeSet(badAir);\n+      //airSet.remove(badAir);\n+      firstCallback.removeAirSet(badAir);\n     }\n \n     if (Sage.DBG) System.out.println("CARNY Traitors:" + traitors);\n-    for (int i = 0; i < traitors.size(); i++)\n+    for (int i = 0, traitorsSize = traitors.size(); i < traitorsSize; i++)\n+    {\n       wiz.removeAgent(traitors.get(i));\n+    }\n \n     synchronized (this)\n     {\n+      Agent thisAgent;\n+      Float thisFloat;\n       for (Airing oldAir : swapMap.keySet())\n       {\n         Airing newAir = swapMap.get(oldAir);\n-        if (newWPMap.containsKey(oldAir))\n-          newWPMap.put(newAir, newWPMap.remove(oldAir));\n-        if (airSet.contains(oldAir))\n+        if ((thisFloat = newWPMap.remove(oldAir)) != null)\n+        {\n+          newWPMap.put(newAir, thisFloat);\n+        }\n+        if (airSet.remove(oldAir))\n         {\n-          airSet.remove(oldAir);\n           airSet.add(newAir);\n         }\n-        if (newMustSeeSet.contains(oldAir))\n+        if (newMustSeeSet.remove(oldAir))\n         {\n-          newMustSeeSet.remove(oldAir);\n           newMustSeeSet.add(newAir);\n         }\n-        if (newCauseMap.containsKey(oldAir))\n-          newCauseMap.put(newAir, newCauseMap.remove(oldAir));\n+        if ((thisAgent = newCauseMap.remove(oldAir)) != null)\n+        {\n+          newCauseMap.put(newAir, thisAgent);\n+        }\n       }\n       wpMap = newWPMap;\n       causeMap = newCauseMap;\n       pots = airSet.toArray(Pooler.EMPTY_AIRING_ARRAY);\n       mustSeeSet = newMustSeeSet;\n     }\n-    // This is when we need to propogate the change to all of our\n+    // This is when we need to propagate the change to all of our\n     // clients\n     clientSyncAll();\n \n     prepped = true;\n     lastCycleCompleteTime = Sage.eventTime();\n \n+    long timeSpan = lastCycleCompleteTime - cycleStartTime;\n+    if (timeSpan < Sage.MILLIS_PER_MIN * 2)\n+      System.out.println("CARNY finished in " + timeSpan + "ms");\n+    else if (timeSpan < Sage.MILLIS_PER_HR * 2)\n+      System.out.println("CARNY finished in " + (timeSpan / 60f / 1000f) + "m");\n+    else\n+      System.out.println("CARNY finished in " + (timeSpan / 60f / 60f / 1000f) + "h");\n+\n     if (doneInit)\n-      Scheduler.getInstance().kick(false);\n+      SchedulerSelector.getInstance().kick(false);\n+\n+    /*dumpResults();\n+    try\n+    {\n+      System.out.println("Press any key to exit...");\n+      System.in.read();\n+    } catch (IOException e)\n+    {\n+      e.printStackTrace();\n+    }\n+    System.exit(0);*/\n   }\n \n   public int getWatchCount() { return globalWatchCount; }\n@@ -1227,21 +1691,38 @@ public final class Carny implements Runnable\n       return Math.max(f, MIN_WP);\n   }\n \n+  public boolean areSameFavorite(Airing a1, Airing a2)\n+  {\n+    return areSameFavorite(a1, a2, null);\n+  }\n+\n   // Returns false iff one is a Favorite and the other isn\'t, or both are favorites with different causes\n-  public synchronized boolean areSameFavorite(Airing a1, Airing a2)\n+  public boolean areSameFavorite(Airing a1, Airing a2, StringBuilder sbCache)\n   {\n-    Agent b1 = causeMap.get(a1);\n-    Agent b2 = causeMap.get(a2);\n+    Agent b1;\n+    Agent b2;\n+    // After we get the values from the map, this isn\'t any different than how the we background\n+    // process data. We accept that things might change a little while we are using them. Otherwise\n+    // we end up snagging agent workers because the followsTrend() can run a little long. The Carny\n+    // workers are also butting heads with each other and Scheduler on the Carny monitor when it is\n+    // doing the potentials sort which could make this an opportunity for a read/write lock, but we\n+    // would need to convert the entire class, so we might do that at a later time. For reference,\n+    // the contention is around 10-200ms per blocked thread which is measurable impact-wise.\n+    synchronized (this)\n+    {\n+      b1 = causeMap.get(a1);\n+      b2 = causeMap.get(a2);\n+    }\n     if (b1 == b2) return true;\n     if (b1 != null && b1.isFavorite())\n     {\n       // The favorite applies for both. One of them may not be in the cause map because\n       // the cause map is limited by the scheduling lookahead. But then we looking further out\n       // in the schedule to conflict resolution we can end up calling this test so we need to check this way too.\n-      return b1.followsTrend(a2, false, null);\n+      return b1.followsTrend(a2, false, sbCache);\n     }\n     if (b2 != null && b2.isFavorite())\n-      return b2.followsTrend(a1, false, null);\n+      return b2.followsTrend(a1, false, sbCache);\n \n     // Neither agent is a favorite; so they are both from the null favorite which is true\n     return true;\n@@ -1459,13 +1940,13 @@ public final class Carny implements Runnable\n     //if (Sage.DBG) System.out.println("IN Carny createPriority(top=" + top + " bottom=" + bottom + ")");\n     top.bully(bottom);\n     //if (Sage.DBG) System.out.println("OUT Carny createPriority(top=" + top + " bottom=" + bottom + ")");\n-    Scheduler.getInstance().kick(false);\n+    SchedulerSelector.getInstance().kick(false);\n   }\n \n   public void setStartPadding(Agent bond, long padAmount)\n   {\n     bond.setStartPadding(padAmount);\n-    Scheduler.getInstance().kick(false);\n+    SchedulerSelector.getInstance().kick(false);\n     sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.FAVORITE_MODIFIED,\n         new Object[] { sage.plugin.PluginEventManager.VAR_FAVORITE, bond });\n   }\n@@ -1473,7 +1954,7 @@ public final class Carny implements Runnable\n   public void setStopPadding(Agent bond, long padAmount)\n   {\n     bond.setStopPadding(padAmount);\n-    Scheduler.getInstance().kick(false);\n+    SchedulerSelector.getInstance().kick(false);\n     sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.FAVORITE_MODIFIED,\n         new Object[] { sage.plugin.PluginEventManager.VAR_FAVORITE, bond });\n   }\n@@ -1481,7 +1962,7 @@ public final class Carny implements Runnable\n   public void setAgentFlags(Agent bond, int flagMask, int flagValue)\n   {\n     bond.setAgentFlags(flagMask, flagValue);\n-    Scheduler.getInstance().kick(false);\n+    SchedulerSelector.getInstance().kick(false);\n     sage.plugin.PluginEventManager.postEvent(sage.plugin.PluginEventManager.FAVORITE_MODIFIED,\n         new Object[] { sage.plugin.PluginEventManager.VAR_FAVORITE, bond });\n   }\n@@ -1643,6 +2124,12 @@ public final class Carny implements Runnable\n \n   private Map<Airing, Airing> swapMap;\n \n+  // This is the queue that all of the worker threads get their tasks from. If we ever need to stop\n+  // everything, all we need to do is clear this queue and all of the threads will stop working\n+  // fairly quickly.\n+  private ConcurrentLinkedQueue<Agent> agentWorkQueue;\n+  private ExecutorService agentWorkers;\n+\n   private static final int strComp(DBObject s1, DBObject s2)\n   {\n     if (s1 == s2) return 0;\n@@ -1818,6 +2305,215 @@ public final class Carny implements Runnable\n \t\tf.setSize(500, 500);\n \t\tf.setVisible(true);\n \t}*/\n+\n+  /*private void dumpResults()\n+  {\n+    System.out.println("Dumping CARNY results...");\n+\n+    List<Airing> localPots;\n+    List<Airing> localLoves;\n+    List<Airing> localMustSee;\n+    List<Map.Entry<Airing, Agent>> localCauseMap;\n+    List<Map.Entry<Airing, Float>> localWpMap;\n+    synchronized (this)\n+    {\n+      localPots = new ArrayList<>(Arrays.asList(pots));\n+      localLoves = new ArrayList<>(loveAirSet);\n+      localMustSee = new ArrayList<>(mustSeeSet);\n+      localCauseMap = new ArrayList<>(causeMap.entrySet());\n+      localWpMap = new ArrayList<>(wpMap.entrySet());\n+    }\n+\n+    FileOutputStream potsFile = null;\n+    FileOutputStream lovesFile = null;\n+    FileOutputStream mustSeeFile = null;\n+    FileOutputStream causeFile = null;\n+    FileOutputStream wpFile = null;\n+\n+    try\n+    {\n+      potsFile = new FileOutputStream("carny_pots.txt", false);\n+      lovesFile = new FileOutputStream("carny_loves.txt", false);\n+      mustSeeFile = new FileOutputStream("carny_must_see.txt", false);\n+      causeFile = new FileOutputStream("carny_cause.txt", false);\n+      wpFile = new FileOutputStream("carny_wp.txt", false);\n+\n+      Collections.sort(localPots, DBObject.ID_COMPARATOR);\n+      Collections.sort(localLoves, DBObject.ID_COMPARATOR);\n+      Collections.sort(localMustSee, DBObject.ID_COMPARATOR);\n+      Collections.sort(localCauseMap, new Comparator<Map.Entry<Airing, Agent>>()\n+      {\n+        @Override\n+        public int compare(Map.Entry<Airing, Agent> o1, Map.Entry<Airing, Agent> o2)\n+        {\n+          return DBObject.ID_COMPARATOR.compare(o1.getKey(), o1.getKey());\n+        }\n+      });\n+      Collections.sort(localWpMap, new Comparator<Map.Entry<Airing, Float>>()\n+      {\n+        @Override\n+        public int compare(Map.Entry<Airing, Float> o1, Map.Entry<Airing, Float> o2)\n+        {\n+          return DBObject.ID_COMPARATOR.compare(o1.getKey(), o1.getKey());\n+        }\n+      });\n+\n+      for (int i = 0; i < 3; i++)\n+      {\n+        List<Airing> currList;\n+        PrintWriter currWriter;\n+        switch (i)\n+        {\n+          case 0:\n+            currList = localPots;\n+            currWriter = new PrintWriter(potsFile);\n+            break;\n+          case 1:\n+            currList = localLoves;\n+            currWriter = new PrintWriter(lovesFile);\n+            break;\n+          default:\n+            currList = localMustSee;\n+            currWriter = new PrintWriter(mustSeeFile);\n+            break;\n+        }\n+\n+        currWriter.write("Airing");\n+        currWriter.write(\'\\t\');\n+        currWriter.write("Show");\n+        currWriter.write(\'\\n\');\n+\n+        for (Airing airing : currList)\n+        {\n+          Show show = airing.getShow();\n+          if (show != null)\n+          {\n+            currWriter.write(airing.toString());\n+            currWriter.write(\'\\t\');\n+            currWriter.write(show.toString());\n+            currWriter.write(\'\\n\');\n+          }\n+          else\n+          {\n+            currWriter.write(airing.toString());\n+            currWriter.write(\'\\t\');\n+            currWriter.write("S[No Show Available]");\n+            currWriter.write(\'\\n\');\n+          }\n+        }\n+\n+        currWriter.flush();\n+      }\n+\n+      PrintWriter currWriter = new PrintWriter(causeFile);\n+      currWriter.write("Agent");\n+      currWriter.write(\'\\t\');\n+      currWriter.write("Airing");\n+      currWriter.write(\'\\t\');\n+      currWriter.write("Show");\n+      currWriter.write(\'\\n\');\n+\n+      for (Map.Entry<Airing, Agent> entry : localCauseMap)\n+      {\n+        Agent agent = entry.getValue();\n+        Airing airing = entry.getKey();\n+        Show show = airing.getShow();\n+        if (show != null)\n+        {\n+          currWriter.write(agent.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write(airing.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write(show.toString());\n+          currWriter.write(\'\\n\');\n+        }\n+        else\n+        {\n+          currWriter.write(agent.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write(airing.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write("S[No Show Available]");\n+          currWriter.write(\'\\n\');\n+        }\n+      }\n+\n+      currWriter.flush();\n+      currWriter = new PrintWriter(wpFile);\n+      currWriter.write("WP(Float)");\n+      currWriter.write(\'\\t\');\n+      currWriter.write("Airing");\n+      currWriter.write(\'\\t\');\n+      currWriter.write("Show");\n+      currWriter.write(\'\\n\');\n+\n+      for (Map.Entry<Airing, Float> entry : localWpMap)\n+      {\n+        Float floater = entry.getValue();\n+        Airing airing = entry.getKey();\n+        Show show = airing.getShow();\n+        if (show != null)\n+        {\n+          currWriter.write(floater.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write(airing.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write(show.toString());\n+          currWriter.write(\'\\n\');\n+        }\n+        else\n+        {\n+          currWriter.write(floater.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write(airing.toString());\n+          currWriter.write(\'\\t\');\n+          currWriter.write("S[No Show Available]");\n+          currWriter.write(\'\\n\');\n+        }\n+      }\n+\n+      currWriter.flush();\n+    }\n+    catch (Exception e)\n+    {\n+      e.printStackTrace(System.out);\n+    }\n+    finally\n+    {\n+      try\n+      {\n+        if (potsFile != null)\n+          potsFile.close();\n+      } catch (Exception e) {}\n+\n+      try\n+      {\n+        if (lovesFile != null)\n+          lovesFile.close();\n+      } catch (Exception e) {}\n+\n+      try\n+      {\n+        if (mustSeeFile != null)\n+          mustSeeFile.close();\n+      } catch (Exception e) {}\n+\n+      try\n+      {\n+        if (causeFile != null)\n+          causeFile.close();\n+      } catch (Exception e) {}\n+\n+      try\n+      {\n+        if (wpFile != null)\n+          wpFile.close();\n+      } catch (Exception e) {}\n+    }\n+\n+    System.out.println("Dumped CARNY results.");\n+  }*/\n+\n   public static interface ProfilingListener\n   {\n     public boolean updateLoves(Set<Airing> s);\n@@ -1825,4 +2521,789 @@ public final class Carny implements Runnable\n     public boolean updateCauseMap(Map<Airing, Agent> m);\n     public boolean updateWPMap(Map<Airing, Float> m);\n   }\n+\n+  public class CarnyAgentWorker implements Callable<CarnyWorkerCallback>\n+  {\n+    // These are used for reference, but the data will never change, so it is ok for all of the\n+    // threads to share these relatively constant variables.\n+    private final boolean controlCPUUsage;\n+    private final int totalThreads;\n+    private final boolean doneInit;\n+    private final boolean legacyKeyword;\n+    private final boolean aggressiveNegativeProfiling;\n+    private final int mapBitShift;\n+    private final CarnyCache cache;\n+    private final String paidProgRez;\n+    private final Airing allAirs[];\n+    private final Airing remAirs[];\n+    private final Airing watchAirs[];\n+    private final Airing wastedAirs[];\n+\n+    public CarnyAgentWorker(boolean controlCPUUsage, int totalThreads, boolean doneInit,\n+                            boolean legacyKeyword, boolean aggressiveNegativeProfiling, int mapBitShift,\n+                            Airing[] allAirs, Airing[] remAirs, Airing[] watchAirs, Airing[] wastedAirs,\n+                            CarnyCache cache)\n+    {\n+      this.controlCPUUsage = controlCPUUsage;\n+      this.totalThreads = totalThreads;\n+      this.doneInit = doneInit;\n+      this.legacyKeyword = legacyKeyword;\n+      this.aggressiveNegativeProfiling = aggressiveNegativeProfiling;\n+      this.mapBitShift = mapBitShift;\n+      this.cache = cache;\n+      this.paidProgRez = cache.paidProgRez;\n+      this.allAirs = allAirs;\n+      this.remAirs = remAirs;\n+      this.watchAirs = watchAirs;\n+      this.wastedAirs = wastedAirs;\n+    }\n+\n+    // Returns false if the whole queue needs to be stopped for a new job. The above shared\n+    // variables must be local to the method controlling the execution of these callables, so we\n+    // will see the stopping process early message at least once for each processing thread, but\n+    // there will be no concerns about old jobs breaking new jobs.\n+    @Override\n+    public CarnyWorkerCallback call() throws Exception\n+    {\n+      CarnyWorkerCallback callback = new CarnyWorkerCallback();\n+\n+      // Logging is synchronized which in this case causes startup contention that does not need to\n+      // exist.\n+      //if (Sage.DBG) System.out.println("Carny agent worker is starting a new job...");\n+\n+      // Create a few local variable for slightly faster access times.\n+      final CarnyCache cache = this.cache;\n+      final StringBuilder sbCache = cache.sbCache;\n+      // 1000 was selected because we usually grow beyond 500 on a typical pass. We sometimes get\n+      // as big as 15000 depending on what agent this worker ends up getting, but tests have shown\n+      // that allocating larger values can actually slow us down a bit partly because not all\n+      // workers end up dealing with large amounts of airings.\n+      cache.airWorkCache = new CacheList(1000);\n+      final Map<Integer, Airing[]> allAirsMap = cache.allAirsMap;\n+      final Map<Integer, Airing[]> remAirsMap = cache.remAirsMap;\n+\n+      Agent currAgent;\n+      while (true)\n+      {\n+        currAgent = agentWorkQueue.poll();\n+        // The queue is empty.\n+        if (currAgent == null)\n+        {\n+          //if (Sage.DBG) System.out.println("Carny agent worker is out of work.");\n+          break;\n+        }\n+\n+        if (controlCPUUsage)\n+          try {Thread.sleep(SLEEP_PERIOD * totalThreads);} catch (Exception e) {}\n+        if (doneInit)\n+        {\n+          // Check to see if something else is a higher priority to calculate.\n+          // NOTE: If we\'ve been trying to build the profile for a half hour and haven\'t finished then it\'s\n+          // likely that stopping early could cause us to never finish since recordings end/start so often.\n+          // So in that case we do NOT stop processing early and just continue on our merry way!\n+          if (!shouldContinueStdProcessing(Sage.eventTime() - cycleStartTime < 30 * Sage.MILLIS_PER_MIN))\n+          {\n+            if (Sage.DBG)\n+              System.out.println("Carny agent worker is quitting early to start a new job...");\n+            // The callback is not complete by default.\n+            return callback; // just bail, it\'ll pick it all up afterwards\n+          }\n+        }\n+\n+        // This clears the array and then adds all of the hashes for this agent.\n+        currAgent.getHashes(cache.currentHashes, mapBitShift);\n+        if (!currAgent.calcWatchProb(controlCPUUsage, watchAirs, wastedAirs, aggressiveNegativeProfiling, cache))\n+        {\n+          callback.addTraitor(currAgent);\n+          continue;\n+        }\n+\n+        // We are passing true to ignoreDisabledFlag because we have already filtered out the\n+        // disabled agents making this additional check a waste of time.\n+        currAgent.getRelatedAirings(allAirs, controlCPUUsage, true, legacyKeyword, allAirsMap, cache, sbCache);\n+        boolean isFavorite = currAgent.isFavorite();\n+\n+        if (isFavorite)\n+        {\n+          int potsWorkCacheSize = cache.airWorkCache.size;\n+          callback.appendCapacityNewLoveAirSet(potsWorkCacheSize);\n+          for (int i = 0; i < potsWorkCacheSize; i++)\n+            callback.addNewLoveAirSet(cache.airWorkCache.data[i]);\n+\n+          // This only happens for active favorites which should number less than 500, so this isn\'t\n+          // going to amount to anything crazy for the GC. We are trying to take advantage of any\n+          // scratch space left in the currently allocated cache list.\n+          cache.airWorkCache.setOffset();\n+          // Also check the rem airs so we\'re sure we get EVERYTHING in the DB that applies to this\n+          // Favorite included in the group.\n+          //\n+          // We are passing true to ignoreDisabledFlag because we have already filtered out the\n+          // disabled agents making this additional check a waste of time.\n+          currAgent.getRelatedAirings(remAirs, controlCPUUsage, true, legacyKeyword, remAirsMap,\n+            cache, cache.sbCache);\n+\n+          int remWorkCacheSize = cache.airWorkCache.size;\n+          callback.appendCapacityNewLoveAirSet(remWorkCacheSize);\n+          for (int i = cache.airWorkCache.offset; i < remWorkCacheSize; i++)\n+            callback.addNewLoveAirSet(cache.airWorkCache.data[i]);\n+\n+          // Remove the entries we just appended and gets rid of the offset so they don\'t break\n+          // anything below.\n+          cache.airWorkCache.clearOffset();\n+        }\n+\n+        // Check to see if this Agent is a Favorite who has a keep at most limit set with\n+        // manual deleting. In that case, we don\'t add any of its future airings to the\n+        // mustSeeSet or to the pots, but we do put them in the loveAirSet.\n+        boolean dontScheduleThisAgent = false;\n+        if (isFavorite && currAgent.testAgentFlag(Agent.DONT_AUTODELETE_FLAG) &&\n+          currAgent.getAgentFlag(Agent.KEEP_AT_MOST_MASK) > 0)\n+        {\n+          int fileCount = 0;\n+          for (int j = 0, potsWorkCacheSize = cache.airWorkCache.size; j < potsWorkCacheSize; j++)\n+          {\n+            MediaFile mf = wiz.getFileForAiring(cache.airWorkCache.data[j]);\n+            if (mf != null && mf.isCompleteRecording())\n+              fileCount++;\n+          }\n+          int keepAtMost = currAgent.getAgentFlag(Agent.KEEP_AT_MOST_MASK);\n+          if (fileCount >= keepAtMost)\n+          {\n+            dontScheduleThisAgent = true;\n+          }\n+        }\n+\n+        int potsWorkCacheSize = cache.airWorkCache.size;\n+        boolean negator = currAgent.isNegativeNelly();\n+        if (negator)\n+          callback.appendCapacityBlackBalled(potsWorkCacheSize);\n+        else\n+          callback.appendCapacityAirSet(potsWorkCacheSize);\n+\n+        for (int j = 0; j < potsWorkCacheSize; j++)\n+        {\n+          Airing agentPot = cache.airWorkCache.data[j];\n+          if (negator)\n+          {\n+            callback.addBlackBalled(agentPot);\n+            continue;\n+          }\n+\n+          // Skip this stuff\n+          Show show = agentPot.getShow();\n+          if ((show != null && show.title.equalsIgnoreCase(paidProgRez)) || wiz.isNoShow(agentPot.showID))\n+            continue;\n+\n+          callback.addAirSet(agentPot);\n+\n+          /*\n+           * NOTE: On 7/22/03 I moved the isWatched() down from the beginning of this loop.\n+           * We want to keep watched airings in the Agent lookup table so we can\n+           * use the agentFlags on them even after they\'re watched. So now watched\n+           * airings will have a cause agent, but their WP will be zero.\n+           */\n+          /*\n+           * NOTE: Narflex - 8/4/08 - Fixed bug where we wouldn\'t use the higher priority favorite\n+           * in the cause map since comparing watchProb would be equal; this would then cause scheduling\n+           * issues because the wrong agent was used for the scheduling that should have been higher priority sometimes\n+           */\n+\n+          // This get evaluated at least once for all airings, so let\'s just doing it here and use\n+          // this value.\n+          boolean isWatchedForSchedulingPurposes = agentPot.isWatchedForSchedulingPurpose();\n+\n+          // This is a more efficient object than what we had when we were maintaining two maps that\n+          // basically work with the same information presented different ways. This also cuts down\n+          // on the Float object creation until after we are done processing all of the agents.\n+          WPCauseValue causeValue = callback.addOrReturnWPCauseValue(agentPot, currAgent);\n+\n+          // We just added this agent.\n+          if (causeValue == null)\n+          {\n+            if (isWatchedForSchedulingPurposes)\n+              callback.addWatchedPotsToClear(agentPot);\n+          }\n+          else if (causeValue.compareAndReplace(currAgent, false) && isWatchedForSchedulingPurposes)\n+          {\n+            callback.addWatchedPotsToClear(agentPot);\n+          }\n+\n+          if (isWatchedForSchedulingPurposes)\n+            continue;\n+          if (dontScheduleThisAgent)\n+          {\n+            MediaFile mf = wiz.getFileForAiring(agentPot);\n+            if (mf == null || !mf.isCompleteRecording())\n+            {\n+              callback.addWatchedPotsToClear(agentPot);\n+              continue;\n+            }\n+          }\n+          if (isFavorite)\n+            callback.addNewMustSeeSet(agentPot);\n+        }\n+      }\n+\n+      callback.setComplete();\n+      return callback;\n+    }\n+  }\n+\n+  public class CarnyWorkerCallback\n+  {\n+    private final ArrayList<WPCauseValue> newWPCauses = new ArrayList<>();\n+    private final ArrayList<Agent> traitors = new ArrayList<>();\n+    // There are always a lot of these.\n+    private final ArrayList<Airing> newLoveAirSet = new ArrayList<>();\n+    private final ArrayList<Airing> blackBalled = new ArrayList<>();\n+    // This gets particularly large making it more ideal to use a HashSet instead of a list.\n+    private final ArrayList<Airing> airSet = new ArrayList<>();\n+    // There are always even more of these.\n+    private final ArrayList<Airing> watchedPotsToClear = new ArrayList<>();\n+    private final ArrayList<Airing> newMustSeeSet = new ArrayList<>();\n+    private boolean complete = false;\n+\n+    public void setComplete()\n+    {\n+      complete = true;\n+    }\n+\n+    public WPCauseValue replaceWPCauseValue(Airing airing, Agent agent)\n+    {\n+      return binaryReplace(newWPCauses, airing, agent);\n+    }\n+\n+    public WPCauseValue addOrReturnWPCauseValue(WPCauseValue newEntry)\n+    {\n+      return binaryInsertOrReturn(newWPCauses, newEntry.airing, newEntry.agent, newEntry);\n+    }\n+\n+    public WPCauseValue addOrReturnWPCauseValue(Airing airing, Agent agent)\n+    {\n+      return binaryInsertOrReturn(newWPCauses, airing, agent, null);\n+    }\n+\n+    public void addTraitor(Agent traitor)\n+    {\n+      // We are iterating though these once, so this is the only thing that will always be unique.\n+      traitors.add(traitor);\n+    }\n+\n+    public void appendCapacityNewLoveAirSet(int size)\n+    {\n+      newLoveAirSet.ensureCapacity(newLoveAirSet.size() + size);\n+    }\n+\n+    public void addNewLoveAirSet(Airing airing)\n+    {\n+      binaryInsert(newLoveAirSet, airing);\n+    }\n+\n+    public void appendCapacityBlackBalled(int size)\n+    {\n+      blackBalled.ensureCapacity(blackBalled.size() + size);\n+    }\n+\n+    public void addBlackBalled(Airing airing)\n+    {\n+      binaryInsert(blackBalled, airing);\n+    }\n+\n+    public void appendCapacityAirSet(int size)\n+    {\n+      airSet.ensureCapacity(airSet.size() + size);\n+    }\n+\n+    public void addAirSet(Airing airing)\n+    {\n+      binaryInsert(airSet, airing);\n+    }\n+\n+    public void removeAirSet(Airing airing)\n+    {\n+      binaryRemove(airSet, airing);\n+    }\n+\n+    public void addWatchedPotsToClear(Airing airing)\n+    {\n+      binaryInsert(watchedPotsToClear, airing);\n+    }\n+\n+    public void addNewMustSeeSet(Airing airing)\n+    {\n+      binaryInsert(newMustSeeSet, airing);\n+    }\n+\n+    public void removeNewMustSeeSet(Airing airing)\n+    {\n+      binaryRemove(newMustSeeSet, airing);\n+    }\n+\n+    private WPCauseValue binaryReplace(List<WPCauseValue> list, Airing airing, Agent agent)\n+    {\n+      int id = airing.getID();\n+      int low = 0;\n+      int high = list.size() - 1;\n+\n+      while (low <= high)\n+      {\n+        int mid = (low + high) >>> 1;\n+        WPCauseValue causeValue = list.get(mid);\n+        int midVal = causeValue.airing.getID();\n+\n+        if (midVal < id)\n+          low = mid + 1;\n+        else if (midVal > id)\n+          high = mid - 1;\n+        else\n+        {\n+          WPCauseValue newValue = new WPCauseValue(agent, airing);\n+          list.set(mid, newValue); // key found\n+          return newValue;\n+        }\n+      }\n+      WPCauseValue newValue = new WPCauseValue(agent, airing);\n+      list.add(low, newValue); // key not found.\n+      return newValue;\n+    }\n+\n+    private WPCauseValue binaryInsertOrReturn(List<WPCauseValue> list, Airing airing, Agent agent, WPCauseValue newEntry)\n+    {\n+      int id = airing.getID();\n+      int low = 0;\n+      int high = list.size() - 1;\n+\n+      while (low <= high)\n+      {\n+        int mid = (low + high) >>> 1;\n+        WPCauseValue causeValue = list.get(mid);\n+        int midVal = causeValue.airing.getID();\n+\n+        if (midVal < id)\n+          low = mid + 1;\n+        else if (midVal > id)\n+          high = mid - 1;\n+        else\n+          return causeValue; // key found\n+      }\n+      list.add(low, newEntry != null ? newEntry : new WPCauseValue(agent, airing)); // key not found.\n+      // Return null if we added the agent since we have nothing further to do with it in that case.\n+      return null;\n+    }\n+\n+    private void binaryInsert(List<Airing> list, Airing airing)\n+    {\n+      int id = airing.getID();\n+      int low = 0;\n+      int high = list.size() - 1;\n+\n+      while (low <= high)\n+      {\n+        int mid = (low + high) >>> 1;\n+        int midVal = list.get(mid).getID();\n+\n+        if (midVal < id)\n+          low = mid + 1;\n+        else if (midVal > id)\n+          high = mid - 1;\n+        else\n+          return; // key found\n+      }\n+      list.add(low, airing); // key not found.\n+    }\n+\n+    private void binaryRemove(List<Airing> list, Airing airing)\n+    {\n+      int id = airing.getID();\n+      int low = 0;\n+      int high = list.size() - 1;\n+\n+      while (low <= high)\n+      {\n+        int mid = (low + high) >>> 1;\n+        int midVal = list.get(mid).getID();\n+\n+        if (midVal < id)\n+          low = mid + 1;\n+        else if (midVal > id)\n+          high = mid - 1;\n+        else\n+        {\n+          list.remove(mid);\n+          return;\n+        }; // key found\n+      }\n+      // key not found.\n+    }\n+  }\n+\n+  public class WPCauseValue\n+  {\n+    // We are keying on airing.\n+    final private Airing airing;\n+    private Agent agent;\n+    private float wp;\n+\n+    public WPCauseValue(Agent agent, Airing airing)\n+    {\n+      this.airing = airing;\n+      this.agent = agent;\n+      this.wp = agent != null ? agent.watchProb : MIN_WP;\n+    }\n+\n+    /**\n+     * Compares the provided agent against the current agent and replaces it and the watch\n+     * probability of the compared agent is more desirable.\n+     *\n+     * @param compareAgent The agent to compare.\n+     * @param order Use {@link #AGENT_SORTER} to ensure agents are also prioritized by their sorted\n+     *             order when they have the same watch potential.\n+     * @return <code>true</code> if the agent was replaced.\n+     */\n+    public boolean compareAndReplace(Agent compareAgent, boolean order)\n+    {\n+      float compareAgentWP = compareAgent.watchProb;\n+      if (agent == null || compareAgentWP > wp)\n+      {\n+        agent = compareAgent;\n+        wp = compareAgentWP;\n+        return true;\n+      }\n+\n+      if (compareAgentWP == wp && (compareAgent.isFavorite() || order))\n+      {\n+        // Check for agent priority. This returns the higher priority agent if there is one or null\n+        // if no explicit priority is defined. When this is null there is an implicit priority that\n+        // based on the AGENT_SORTER comparator. When order is true, we considering this sorting\n+        // order in the determination on if we need to replace the agent or not.\n+        Agent winner = doBattle(agent, compareAgent);\n+        if (winner == compareAgent ||\n+          order && winner == null && AGENT_SORTER.compare(agent, compareAgent) > 0)\n+        {\n+          agent = compareAgent;\n+          wp = compareAgentWP;\n+          return true;\n+        }\n+      }\n+\n+      return false;\n+    }\n+  }\n+\n+  public class CarnyCache\n+  {\n+    // Shared.\n+    final String paidProgRez;\n+    final Map<Integer, Airing[]> allAirsMap;\n+    final Map<Integer, Airing[]> remAirsMap;\n+    final Map<Integer, Airing[]> watchAirsMap;\n+    final Map<Integer, Wasted[]> wastedAirsMap;\n+    final int hashZero;\n+    final boolean useMaps;\n+\n+    // Per thread.\n+    final StringBuilder sbCache = new StringBuilder();\n+    // This is the array that will actually be used and will return our results.\n+    CacheList airWorkCache = null;\n+    // This is the hashes we are working with for the current agent. This prevents us from getting\n+    // this list up to 4 times and the GC associated with it.\n+    final List<Integer> currentHashes = new ArrayList<>();\n+\n+    // It\'s actually faster to cache and clear these between runs of watchProb().\n+    final Map<Stringer, MutableFloat> titleWatchMap = new HashMap<>();\n+    final Map<Stringer, MutableInteger> titleAllMap = new HashMap<>();\n+\n+    // Clone the shared parts of the cache.\n+    public CarnyCache(CarnyCache cache)\n+    {\n+      this(cache.allAirsMap, cache.remAirsMap, cache.watchAirsMap, cache.wastedAirsMap,\n+        cache.paidProgRez, cache.hashZero, cache.useMaps);\n+    }\n+\n+    // Create a new shared cache.\n+    public CarnyCache(Map<Integer, Airing[]> allAirsMap, Map<Integer, Airing[]> remAirsMap,\n+                      Map<Integer, Airing[]> watchAirsMap, Map<Integer, Wasted[]> wastedAirsMap)\n+    {\n+      this(allAirsMap, remAirsMap, watchAirsMap, wastedAirsMap,\n+        Sage.rez("Paid_Programming").toLowerCase(),\n+        (allAirsMap != null && remAirsMap != null && watchAirsMap != null && wastedAirsMap != null &&\n+          allAirsMap.get(0) == null && remAirsMap.get(0) == null &&\n+          watchAirsMap.get(0) == null && wastedAirsMap.get(0) == null) ? 0 : -1,\n+        (allAirsMap != null && remAirsMap != null && watchAirsMap != null && wastedAirsMap != null));\n+    }\n+\n+    private CarnyCache(Map<Integer, Airing[]> allAirsMap, Map<Integer, Airing[]> remAirsMap,\n+                       Map<Integer, Airing[]> watchAirsMap, Map<Integer, Wasted[]> wastedAirsMap,\n+                       String paidProgRez, int hashZero, boolean useMaps)\n+    {\n+      this.paidProgRez = paidProgRez;\n+      this.allAirsMap = allAirsMap;\n+      this.remAirsMap = remAirsMap;\n+      this.watchAirsMap = watchAirsMap;\n+      this.wastedAirsMap = wastedAirsMap;\n+\n+      // If we know none of the maps have anything mapped to zero, we don\'t need to check this for\n+      // anything and this gives us a small performance bump.\n+      this.hashZero = hashZero;\n+      this.useMaps = useMaps;\n+    }\n+  }\n+\n+  /**\n+   * This is used for faster operations done by the agents that require array storage.\n+   */\n+  public static class CacheList\n+  {\n+    // All binary searches must use offset as their starting index.\n+    int offset = 0;\n+    // This number must not be adjusted to accommodate the apparent size by subtracting\n+    // offset.\n+    int size = 0;\n+    Airing data[];\n+\n+    public CacheList(int size)\n+    {\n+      this.data = new Airing[size];\n+    }\n+\n+    /**\n+     * Ensures that the requested capacity is free in the array.\n+     * <p/>\n+     * The provided capacity is added to the current size of the array and if the resulting number\n+     * is bigger than the currently allocated array, a bigger array will be allocated to accommodate\n+     * the anticipated data.\n+     *\n+     * @param totalNeeded Additional capacity required.\n+     */\n+    public void ensureAddCapacity(int totalNeeded)\n+    {\n+      totalNeeded += size;\n+      if (totalNeeded > data.length)\n+      {\n+        int newSize = data.length + 1000;\n+        if (newSize < 0 /*overflow*/ || newSize < totalNeeded)\n+          newSize = totalNeeded;\n+        if (newSize == Integer.MAX_VALUE && data.length == Integer.MAX_VALUE)\n+          throw new OutOfMemoryError();\n+        data = Arrays.copyOf(data, newSize);\n+      }\n+    }\n+\n+    /**\n+     * Binary sort the array.\n+     */\n+    public void sort()\n+    {\n+      Arrays.sort(data, offset, size, DBObject.ID_COMPARATOR);\n+    }\n+\n+    /**\n+     * Search for an airing based between the current offset and size.\n+     *\n+     * @param key The airing to look up.\n+     * @return <code>true</code> if the airing was found.\n+     */\n+    public int binarySearch(Airing key)\n+    {\n+      return binarySearch(data, offset, size, key);\n+    }\n+\n+    /**\n+     * Search for an airing based between the current offset and specified end (exclusive).\n+     *\n+     * @param end The end of the range to search.\n+     * @param key The airing to look up.\n+     * @return <code>true</code> if the airing was found in the requested range.\n+     */\n+    public int binarySearch(int end, Airing key)\n+    {\n+      return binarySearch(data, offset, end, key);\n+    }\n+\n+    /**\n+     * Search for an airing based between the specified start (inclusive) and specified end\n+     * (exclusive).\n+     *\n+     * @param start The start of the range to search.\n+     * @param end The end of the range to search.\n+     * @param key The airing to look up.\n+     * @return <code>true</code> if the airing was found in the requested range.\n+     */\n+    public int binarySearch(int start, int end, Airing key)\n+    {\n+      return binarySearch(data, start, end, key);\n+    }\n+\n+    private static int binarySearch(Airing array[], int start, int end, Airing key)\n+    {\n+      int id = key.getID();\n+      int low = start;\n+      int high = end - 1;\n+\n+      while (low <= high)\n+      {\n+        int mid = (low + high) >>> 1;\n+        int midVal = array[mid].getID();\n+\n+        if (midVal < id)\n+          low = mid + 1;\n+        else if (midVal > id)\n+          high = mid - 1;\n+        else\n+          return mid;\n+      }\n+\n+      if (Agent.VERIFY_AIRING_OPTIMIZATION)\n+      {\n+        // This is only a problem if the issue is within the range we asked for.\n+        int index = -1;\n+        for (int i = start; i < end; i++)\n+        {\n+          if (array[i].equals(key))\n+          {\n+            index = i;\n+            break;\n+          }\n+        }\n+\n+        if (index != -1)\n+        {\n+          StringBuilder stringBuilder = new StringBuilder("FAILED: to match what should have matched: ");\n+          stringBuilder.append(key.getID()).append(" is in: ");\n+          for (int i = start; i < end; i++)\n+          {\n+            Airing object = array[i];\n+            if (object == key)\n+              stringBuilder.append("****");\n+            stringBuilder.append(object.getID()).append(\',\');\n+          }\n+          System.out.println(stringBuilder);\n+        }\n+      }\n+\n+      return -(low + 1);\n+    }\n+\n+    /**\n+     * Add new airing only if a binary search returns that the airing does not already exist.\n+     * <p/>\n+     * This will insert the airing binary sorted. This will also ensure the required capacity to add\n+     * if it needs to add the airing.\n+     *\n+     * @param airing The airing to add.\n+     * @return <code>true</code> if the airing was added.\n+     */\n+    public boolean binaryAdd(Airing airing)\n+    {\n+      int index = binarySearch(airing);\n+      if (index < 0)\n+      {\n+        index = -(index + 1);\n+        ensureAddCapacity(1);\n+        add(index, airing);\n+        return true;\n+      }\n+      return false;\n+    }\n+\n+    /**\n+     * Add a new airing to the end of this array.\n+     * <p/>\n+     * This method has no implicit sorting.\n+     *\n+     * @param airing The airing to add.\n+     */\n+    public void add(Airing airing)\n+    {\n+      data[size++] = airing;\n+    }\n+\n+    /**\n+     * Add a new airing at a specific index.\n+     *\n+     * @param index The index.\n+     * @param airing The airing to add.\n+     */\n+    public void add(int index, Airing airing)\n+    {\n+      // We do this when appropriate instead of on every add.\n+      //ensureCacheCapacity(useWorkCacheSize + index);\n+      System.arraycopy(data, index, data, index + 1, size - index);\n+      data[index] = airing;\n+      size++;\n+    }\n+\n+    /**\n+     * Append a range of an existing array to this array.\n+     *\n+     * @param src The source array.\n+     * @param start The starting index to copy (inclusive).\n+     * @param end The ending index to copy (exclusive).\n+     */\n+    public void add(Airing src[], int start, int end)\n+    {\n+      int len = end - start;\n+      System.arraycopy(src, start, data, size, len);\n+      size += len;\n+    }\n+\n+    /**\n+     * Defines the current cache offset to be the current size of the cache.\n+     */\n+    public void setOffset()\n+    {\n+      offset = size;\n+    }\n+\n+    /**\n+     * Clears the cache offset defined by {@link #setOffset()} and resets the size to the last\n+     * cache offset.\n+     */\n+    public void clearOffset()\n+    {\n+      size = offset;\n+      offset = 0;\n+    }\n+\n+    /**\n+     * Resets the cache to the last offset. (not necessarily 0)\n+     */\n+    public void clear()\n+    {\n+      size = offset;\n+    }\n+\n+    /**\n+     * Get the apparent cache size.\n+     * <p/>\n+     * This is not to be used for to determine what the next index will be. The next index is always\n+     * based on zero regardless of the offset for performance reasons.\n+     *\n+     * @return The cache size adjusted for the current offset.\n+     */\n+    public int apparentSize()\n+    {\n+      return size - offset;\n+    }\n+\n+    /**\n+     * Create and return a copy of this list.\n+     */\n+    public CacheList copy()\n+    {\n+      CacheList list = new CacheList(data.length);\n+      System.arraycopy(data, 0, list.data, 0, size);\n+      list.size = size;\n+      list.offset = offset;\n+      return list;\n+    }\n+\n+    /**\n+     * Get a list representation of this array.\n+     */\n+    public List<Airing> getList()\n+    {\n+      return Arrays.asList(data).subList(offset, size);\n+    }\n+  }\n }\ndiff --git a/java/sage/EPG.java b/java/sage/EPG.java\nindex bdef06fa..9af132fa 100644\n--- a/java/sage/EPG.java\n+++ b/java/sage/EPG.java\n@@ -16,6 +16,8 @@\n package sage;\n \n import java.lang.reflect.Constructor;\n+import java.util.concurrent.locks.ReadWriteLock;\n+import java.util.concurrent.locks.ReentrantReadWriteLock;\n \n import sage.Wizard.MaintenanceType;\n import sage.epg.sd.SDRipper;\n@@ -409,17 +411,22 @@ public final class EPG implements Runnable\n \n   void resetViewableStationsCache()\n   {\n-    synchronized (stationIDCacheLock)\n+    stationIDCacheLock.writeLock().lock();\n+    try\n     {\n       viewableStationIDCache = null;\n       VideoFrame.clearSortedChannelLists();\n     }\n+    finally\n+    {\n+      stationIDCacheLock.writeLock().unlock();\n+    }\n     // NOTE: We didn\'t need this in our old UI because we would call RemoveUnusedLineups whenever someone was exiting the\n     // channel setup UIs which would then kick the scheduler. Now that it\'s happening via backend processes we need to kick it\n     // every time we change the enabled channels so it redoes the schedule and then also kicks the Seeker for what channels are viewable.\n     if (!Sage.client)\n     {\n-      Scheduler.getInstance().kick(true);\n+      SchedulerSelector.getInstance().kick(true);\n     }\n   }\n \n@@ -637,7 +644,7 @@ public final class EPG implements Runnable\n         if (reqMaintenanceType != MaintenanceType.NONE)\n         {\n           Carny.getInstance().kickHard();\n-          Scheduler.getInstance().kick(false);\n+          SchedulerSelector.getInstance().kick(false);\n         }\n \n         if (reqMaintenanceType != MaintenanceType.NONE\n@@ -1473,7 +1480,8 @@ public final class EPG implements Runnable\n \n   private void createViewableStationIDCache()\n   {\n-    synchronized (stationIDCacheLock)\n+    stationIDCacheLock.writeLock().lock();\n+    try\n     {\n       java.util.HashSet<Integer> viewableStations = new java.util.HashSet<Integer>();\n       for (int i = 0; i < sources.size(); i++)\n@@ -1490,11 +1498,12 @@ public final class EPG implements Runnable\n             while (walker.hasNext())\n             {\n               java.util.Map.Entry<Integer, String[]> ent = walker.next();\n-              if (!ds.unavailStations.contains(ent.getKey()))\n+              Integer key = ent.getKey();\n+              if (!ds.unavailStations.contains(key))\n               {\n                 String[] val = ent.getValue();\n                 if (val.length > 0 && val[0].length() > 0)\n-                  viewableStations.add(ent.getKey());\n+                  viewableStations.add(key);\n               }\n             }\n           }\n@@ -1511,16 +1520,40 @@ public final class EPG implements Runnable\n       }\n       java.util.Arrays.sort(viewableStationIDCache);\n     }\n+    finally\n+    {\n+      stationIDCacheLock.writeLock().unlock();\n+    }\n   }\n \n   public boolean canViewStation(int stationID)\n   {\n-    synchronized (stationIDCacheLock)\n+    // There is significant contention on this lock when Carny is multi-threaded, so we now use a\n+    // read/write lock.\n+    stationIDCacheLock.readLock().lock();\n+    try\n+    {\n+      if (viewableStationIDCache != null)\n+        return java.util.Arrays.binarySearch(viewableStationIDCache, stationID) >= 0;\n+    }\n+    finally\n+    {\n+      stationIDCacheLock.readLock().unlock();\n+    }\n+\n+    // This only happens when we change what\'s viewable which isn\'t that often for a normal user, so\n+    // this is an acceptable performance loss.\n+    stationIDCacheLock.writeLock().lock();\n+    try\n     {\n       if (viewableStationIDCache == null)\n         createViewableStationIDCache();\n       return java.util.Arrays.binarySearch(viewableStationIDCache, stationID) >= 0;\n     }\n+    finally\n+    {\n+      stationIDCacheLock.writeLock().unlock();\n+    }\n   }\n   /*for (int i = 0; i < sources.size(); i++)\n \t\t{\n@@ -1665,7 +1698,8 @@ public final class EPG implements Runnable\n   private java.util.Map<String, java.io.File> logoMap;\n   //\tprivate java.util.Map colorMap;\n \n-  private Object stationIDCacheLock = new Object();\n+  // Now that Carny is multi-threaded, there is significant reader contention for this cache.\n+  private final ReadWriteLock stationIDCacheLock = new ReentrantReadWriteLock();\n   private int[] viewableStationIDCache;\n \n   private final java.util.Map<Long, java.util.Map<Integer, String[]>> lineups;\ndiff --git a/java/sage/EPGDataSource.java b/java/sage/EPGDataSource.java\nindex 0fc5b015..e7268c04 100644\n--- a/java/sage/EPGDataSource.java\n+++ b/java/sage/EPGDataSource.java\n@@ -133,7 +133,7 @@ public class EPGDataSource\n       if (foundOne)\n       {\n         dataScanRequested = true;\n-        Seeker.getInstance().kick();\n+        SeekerSelector.getInstance().kick();\n       }\n       return true;\n     }\n@@ -159,7 +159,7 @@ public class EPGDataSource\n         cdi = cdis[i];\n     }\n     if (kickSeekNow)\n-      Seeker.getInstance().kick();\n+      SeekerSelector.getInstance().kick();\n     if (dataScanAllowed && cdi != null && cdi.isActive() && cdi.getCaptureDevice().isDataScanning())\n     {\n       if (Sage.DBG) System.out.println("EPGDS " + name + " found a capture device to start data scanning with:" + cdi);\n@@ -217,7 +217,7 @@ public class EPGDataSource\n         java.util.List<Integer> currStatList = ent.getValue();\n \n         // If we\'re here then we want to scan!\n-        synchronized (Seeker.getInstance())\n+        synchronized (SeekerSelector.getInstance())\n         {\n           if (cdi.getCaptureDevice().isDataScanning())\n           {\n@@ -254,7 +254,7 @@ public class EPGDataSource\n       for (int i = 0; i < cdis.length; i++)\n         cdis[i].getCaptureDevice().cancelDataScanRequest(cdis[i]);\n       dataScanRequested = false;\n-      Seeker.getInstance().kick();\n+      SeekerSelector.getInstance().kick();\n     }\n   }\n \n@@ -702,7 +702,7 @@ public class EPGDataSource\n         if (foundOne)\n         {\n           dataScanRequested = true;\n-          Seeker.getInstance().kick();\n+          SeekerSelector.getInstance().kick();\n         }\n       }\n \ndiff --git a/java/sage/FSManager.java b/java/sage/FSManager.java\nindex 8e831a46..1ab36c22 100644\n--- a/java/sage/FSManager.java\n+++ b/java/sage/FSManager.java\n@@ -51,7 +51,22 @@ public class FSManager implements Runnable\n       while (walker.hasNext())\n       {\n         java.io.File newF = new java.io.File(walker.next().toString());\n-        Seeker.getInstance().addIgnoreFile(newF);\n+\n+        // There are some initialization ordering issues if we route through the selector here\n+        // instead of using Library directly. The Seeker2 singleton is always loaded before the\n+        // Library singleton in the selector and Seeker2 loads FSManager (hence why we are here). If\n+        // we call back to the selector from here the Library reference doesn\'t exist yet and we\n+        // will get a null pointer exception. It is however not a problem to directly get the\n+        // Library singleton from here. It\'s important to note that while this sounds a little hacky\n+        // the selector is there for performance reasons and this is the only case where we have an\n+        // exception. I could just re-order a few things, but then I might accidentally change how\n+        // something worked in the old Seeker and I do not want to create any new problems to\n+        // address one exception.\n+        // TODO: Will be enabled in a future commit.\n+        /*if (SeekerSelector.USE_BETA_SEEKER)\n+          Library.getInstance().addIgnoreFile(newF);\n+        else*/\n+          Seeker.getInstance().addIgnoreFile(newF);\n       }\n     }\n     // Make sure we mount the hard drive as early as possible if we know it\'s there; the Seeker needs this for its lib scan\n@@ -118,7 +133,7 @@ public class FSManager implements Runnable\n         deleteQueue.notifyAll();\n       }\n     }\n-    Seeker seeky = Seeker.getInstance();\n+    Hunter seeky = SeekerSelector.getInstance();\n     long checkPeriod = Sage.getLong("seeker/fs_mgr_wait_period", 15000);\n     while (alive)\n     {\n@@ -285,7 +300,7 @@ public class FSManager implements Runnable\n         }\n       }\n \n-      java.io.File[] netMounts = Seeker.getInstance().getSmbMountedFolders();\n+      java.io.File[] netMounts = SeekerSelector.getInstance().getSmbMountedFolders();\n       for (int i = 0; i < netMounts.length; i++)\n       {\n         java.io.File stvTmpDir = new java.io.File(netMounts[i], ".sagetv");\n@@ -562,7 +577,7 @@ public class FSManager implements Runnable\n                 {\n                   deleteQueue.remove(0);\n                   Sage.put(DELETE_QUEUE_PROP, Sage.createDelimSetString(new java.util.HashSet(deleteQueue), ";"));\n-                  Seeker.getInstance().removeIgnoreFile(currFile.f);\n+                  SeekerSelector.getInstance().removeIgnoreFile(currFile.f);\n                 }\n                 Sage.savePrefs();\n                 if (Sage.DBG) System.out.println("Completed progressive deletion of: " + currFile);\n@@ -624,7 +639,7 @@ public class FSManager implements Runnable\n         dinf.firstFailTime = Sage.time();\n         deleteQueue.add(dinf);\n         deleteQueue.notifyAll();\n-        Seeker.getInstance().addIgnoreFile(f);\n+        SeekerSelector.getInstance().addIgnoreFile(f);\n         return true;\n       }\n     }\ndiff --git a/java/sage/FileTransfer.java b/java/sage/FileTransfer.java\nindex a80ef129..4a8009f6 100644\n--- a/java/sage/FileTransfer.java\n+++ b/java/sage/FileTransfer.java\n@@ -180,22 +180,22 @@ public class FileTransfer extends SystemTask\n \n       if (totalFileSize > freeSpace)\n       {\n-        if (!Sage.WINDOWS_OS || Seeker.getInstance().isPathInManagedStorage(destFile))\n+        if (!Sage.WINDOWS_OS || SeekerSelector.getInstance().isPathInManagedStorage(destFile))\n         {\n           // Make a storage request with Seeker to get the space we need, and then see if we\'ve got the space\n           // now and then cancel the request.\n           if (Sage.DBG) System.out.println("Requesting Seeker to clear up " + ((totalFileSize - freeSpace)/1000000) + "MB worth of space");\n-          java.io.File tempFile = Seeker.getInstance().requestDirectoryStorage("scratch", totalFileSize - freeSpace);\n-          synchronized (Seeker.getInstance())\n+          java.io.File tempFile = SeekerSelector.getInstance().requestDirectoryStorage("scratch", totalFileSize - freeSpace);\n+          synchronized (SeekerSelector.getInstance())\n           {\n-            Seeker.getInstance().kick();\n+            SeekerSelector.getInstance().kick();\n             try\n             {\n-              Seeker.getInstance().wait(5000);\n+              SeekerSelector.getInstance().wait(5000);\n             }catch (InterruptedException e){}\n           }\n           freeSpace = Sage.getDiskFreeSpace(destFile.getAbsolutePath());\n-          Seeker.getInstance().clearDirectoryStorageRequest(tempFile);\n+          SeekerSelector.getInstance().clearDirectoryStorageRequest(tempFile);\n           if (totalFileSize > freeSpace)\n           {\n             if (Sage.DBG) System.out.println("Unable to clear up enough free space for library import");\n@@ -399,7 +399,7 @@ public class FileTransfer extends SystemTask\n       int fileStats = mcsr.fsGetPathAttributes(currPath);\n       if ((fileStats & MiniClientSageRenderer.FS_PATH_FILE) != 0)\n       {\n-        if (allFileTypes || Seeker.getInstance().hasImportableFileExtension(currPath))\n+        if (allFileTypes || SeekerSelector.getInstance().hasImportableFileExtension(currPath))\n           rv.add(currPath);\n       }\n       else if ((fileStats & MiniClientSageRenderer.FS_PATH_DIRECTORY) != 0)\ndiff --git a/java/sage/Hunter.java b/java/sage/Hunter.java\nnew file mode 100644\nindex 00000000..d0961503\n--- /dev/null\n+++ b/java/sage/Hunter.java\n@@ -0,0 +1,181 @@\n+/*\n+ * Copyright 2015 The SageTV Authors. All Rights Reserved.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *  http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an "AS IS" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+package sage;\n+\n+import java.io.File;\n+import java.util.Comparator;\n+import java.util.Set;\n+\n+/**\n+ * Hunter is an intermediary interface to switch between Seeker and Seeker2 + Library.\n+ */\n+public interface Hunter extends Runnable\n+{\n+  // *****SEEKER*****\n+  public void kick();\n+\n+  public MediaFile getCurrRecordMediaFile(CaptureDevice capDev);\n+\n+  public MediaFile getCurrRecordFileForClient(UIClient uiClient);\n+\n+  public MediaFile getCurrRecordFileForClient(UIClient uiClient, boolean syncLock);\n+\n+  public CaptureDevice getCaptureDeviceControlledByClient(UIClient uiClient);\n+\n+  public MediaFile[] getCurrRecordFiles();\n+\n+  public boolean isAClientControllingCaptureDevice(CaptureDevice capDev);\n+\n+  public void checkForEncoderHalts();\n+\n+  public boolean isMediaFileBeingViewed(MediaFile mf);\n+\n+  public boolean isPrepped();\n+\n+  public void run();\n+\n+  public void releaseAllEncoding();\n+\n+  public void goodbye();\n+\n+  public void finishWatch(UIClient uiClient);\n+\n+  public int forceChannelTune(String mmcInputName, String chanString, UIClient uiClient);\n+\n+  public MediaFile requestWatch(MediaFile watchFile, int[] errorReturn, UIClient uiClient);\n+\n+  public MediaFile requestWatch(Airing watchAir, int[] errorReturn, UIClient uiClient);\n+\n+  public int record(final Airing watchAir, UIClient uiClient);\n+\n+  public void cancelRecord(final Airing watchAir, UIClient uiClient);\n+\n+  public int modifyRecord(long startTimeModify, long endTimeModify, ManualRecord orgMR, UIClient uiClient);\n+\n+  public int timedRecord(long startTime, long stopTime, int stationID, int recurrence, Airing recAir, UIClient uiClient);\n+\n+  public Airing[] getIRScheduledAirings();\n+\n+  public Object[] getScheduledAirings();\n+\n+  public Airing[] getInterleavedScheduledAirings();\n+\n+  public Airing[] getScheduledAiringsForSource(String sourceName);\n+\n+  public Airing[] getInterleavedScheduledAirings(long startTime, long stopTime);\n+\n+  public Airing[] getScheduledAiringsForSource(String sourceName, long startTime, long stopTime);\n+\n+  public Airing[] getRecentWatches(long time);\n+\n+  public String getDefaultQuality();\n+\n+  public void setDefaultQuality(String newQuality);\n+\n+  public boolean getDisableProfilerRecording();\n+\n+  public void setDisableProfilerRecording(boolean x);\n+\n+  public boolean getUseDTVMajorMinorChans();\n+\n+  /**\n+   * This value is set, but never used for anything. We should get rid of it and remove the API\n+   * function since it serves no purpose.\n+   */\n+  public void setUseDTVMajorMinorChans(boolean x);\n+\n+  public CaptureDeviceInput getInputForCurrRecordingFile(MediaFile mf);\n+\n+  public void requestDeviceReset(CaptureDevice capDev);\n+\n+  public boolean requiresPower();\n+\n+  // *****LIBRARY*****\n+  public boolean hasImportableFileExtension(String s);\n+\n+  public int guessImportedMediaMaskFast(String s);\n+\n+  public int guessImportedMediaMask(String s);\n+\n+  public boolean isAutoImportEnabled();\n+\n+  public boolean isDoingImportScan();\n+\n+  public void scanLibrary(boolean waitTillComplete);\n+\n+  public boolean destroyFile(MediaFile mf, boolean considerInProfile, String reason);\n+\n+  public boolean destroyFile(MediaFile mf, boolean considerInProfile, String reason, String uiContext);\n+\n+  public File[] getArchiveDirectories(int importMask);\n+\n+  public void addArchiveDirectory(String s, int importMask);\n+\n+  public void removeArchiveDirectory(File f, int importMask);\n+\n+  public File[] getSmbMountedFolders();\n+\n+  public boolean isSmbMountedFolder(File f);\n+\n+  /**\n+   * [0] - Used, [1] - Total\n+   */\n+  public long[] getUsedAndTotalVideoDiskspace();\n+\n+  public long getAvailVideoDiskspace();\n+\n+  public long getUsedVideoDiskspace();\n+\n+  public long getUsedImportedLibraryDiskspace();\n+\n+  public long getTotalImportedLibraryDuration();\n+\n+  public void processFileExport(File[] files, byte acquisitionTech);\n+\n+  public boolean requestFileStorage(File theFile, long theSize);\n+\n+  public void clearFileStorageRequest(File theFile);\n+\n+  public File requestDirectoryStorage(String dirName, long theSize);\n+\n+  public void clearDirectoryStorageRequest(File theFile);\n+\n+  public File[] getVideoStoreDirectories();\n+\n+  public String getRuleForDirectory(File testDir);\n+\n+  public long getRuleSizeForDirectory(File testDir);\n+\n+  public void removeVideoDirectory(File testDir);\n+\n+  public void changeVideoDirectory(File oldDir, File testDir, String rule, long size);\n+\n+  public void addVideoDirectory(String testDir, String rule, long size);\n+\n+  public boolean isPathInManagedStorage(File f);\n+\n+  public Set<String> getFailedNetworkMounts();\n+\n+  public Comparator<DBObject> getMediaFileComparator(boolean fast);\n+\n+  public void disableLibraryScanning();\n+\n+  public void addIgnoreFile(File ignoreMe);\n+\n+  public void removeIgnoreFile(File ignoreMe);\n+\n+  public long getTotalVideoDuration();\n+}\ndiff --git a/java/sage/IOUtils.java b/java/sage/IOUtils.java\nindex 92e031d9..7addb586 100644\n--- a/java/sage/IOUtils.java\n+++ b/java/sage/IOUtils.java\n@@ -137,7 +137,7 @@ public class IOUtils\n     {\n       if (kids[i].isFile())\n       {\n-        if (includeAllFileTypes || Seeker.getInstance().hasImportableFileExtension(kids[i].getName()))\n+        if (includeAllFileTypes || SeekerSelector.getInstance().hasImportableFileExtension(kids[i].getName()))\n           rv.add(kids[i]);\n       }\n       else if (kids[i].isDirectory())\ndiff --git a/java/sage/LinuxUtils.java b/java/sage/LinuxUtils.java\nindex ba84ebb7..7fad66ff 100644\n--- a/java/sage/LinuxUtils.java\n+++ b/java/sage/LinuxUtils.java\n@@ -317,7 +317,7 @@ public class LinuxUtils\n                   {\n                     // When we first set the clock we need to kick some parts of the core system if we didn\'t get it set at the start\n                     Carny.getInstance().kickHard();\n-                    Scheduler.getInstance().kick(false);\n+                    SchedulerSelector.getInstance().kick(false);\n                   }\n                   ntpdateWorked = true;\n                 }\ndiff --git a/java/sage/MMC.java b/java/sage/MMC.java\nindex ef637af3..b37c3854 100644\n--- a/java/sage/MMC.java\n+++ b/java/sage/MMC.java\n@@ -119,25 +119,25 @@ public class MMC\n       if (Sage.DBG) System.out.println("EncoderMap=" + globalEncoderMap);\n     }\n     NetworkClient.distributeRecursivePropertyChange("mmc/encoders");\n-    Seeker.getInstance().kick();\n-    Scheduler.getInstance().kick(true);\n+    SeekerSelector.getInstance().kick();\n+    SchedulerSelector.getInstance().kick(true);\n   }\n   \n  // This is an overload of redetectCaptureDevices.  It is meant to only redetect devices for\n  // one CaptureDeviceManager.  For instance discover NetworkEncoder devices\n  public void redetectCaptureDevices(CaptureDeviceManager mgr)\n  {\n-     if (Sage.DBG) System.out.println("MMC is re-doing the capture device detection on " + mgr);\n-     mgr.detectCaptureDevices((CaptureDevice[]) globalEncoderMap.values().toArray(new CaptureDevice[0]));\n-     CaptureDevice[] newDevs = mgr.getCaptureDevices();\n-     \n-     if (Sage.DBG) System.out.println("devices detected=" + java.util.Arrays.asList(newDevs));\n-     updateCaptureDeviceObjects(newDevs);\n+   if (Sage.DBG) System.out.println("MMC is re-doing the capture device detection on " + mgr);\n+   mgr.detectCaptureDevices((CaptureDevice[]) globalEncoderMap.values().toArray(new CaptureDevice[0]));\n+   CaptureDevice[] newDevs = mgr.getCaptureDevices();\n+\n+   if (Sage.DBG) System.out.println("devices detected=" + java.util.Arrays.asList(newDevs));\n+   updateCaptureDeviceObjects(newDevs);\n \n-     if (Sage.DBG) System.out.println("EncoderMap=" + globalEncoderMap);\n-     NetworkClient.distributeRecursivePropertyChange("mmc/encoders");\n-     Seeker.getInstance().kick();\n-     Scheduler.getInstance().kick(true);\n+   if (Sage.DBG) System.out.println("EncoderMap=" + globalEncoderMap);\n+   NetworkClient.distributeRecursivePropertyChange("mmc/encoders");\n+   SeekerSelector.getInstance().kick();\n+   SchedulerSelector.getInstance().kick(true);\n  }\n \n   // If we\'re changing the actual CapDev object than we need to rebuild this map. This occurs\n@@ -406,6 +406,14 @@ public class MMC\n \n   public long getRecordedBytes(java.io.File recordingFile)\n   {\n+    // TODO: Will be enabled in a future commit.\n+    /*if (SeekerSelector.USE_BETA_SEEKER)\n+    {\n+      long returnValue = Splitter.getInstance().getBytesStreamed(recordingFile);\n+      if (returnValue != -1)\n+        return returnValue;\n+    }*/\n+\n     java.util.Iterator walker = globalEncoderMap.values().iterator();\n     while (walker.hasNext())\n     {\ndiff --git a/java/sage/ManualRecord.java b/java/sage/ManualRecord.java\nindex 86513834..e121eba9 100644\n--- a/java/sage/ManualRecord.java\n+++ b/java/sage/ManualRecord.java\n@@ -166,7 +166,12 @@ public final class ManualRecord extends DBObject\n     }\n   }\n \n-  Airing getSchedulingAiring()\n+  public int getStationID()\n+  {\n+    return stationID;\n+  }\n+\n+  public Airing getSchedulingAiring()\n   {\n     if (myAiring == null)\n     {\n@@ -304,6 +309,7 @@ public final class ManualRecord extends DBObject\n \n   public long getStartTime() { return startTime; }\n   public long getEndTime() { return startTime + duration; }\n+  public long getDuration() { return duration; }\n \n   public String toString()\n   {\ndiff --git a/java/sage/MediaFile.java b/java/sage/MediaFile.java\nindex df2d7720..7bbf0ca6 100644\n--- a/java/sage/MediaFile.java\n+++ b/java/sage/MediaFile.java\n@@ -161,11 +161,21 @@ public class MediaFile extends DBObject implements SegmentedFile\n     return liveFileMap.get(cdi);\n   }\n \n+  static MediaFile putLiveMediaFileForInput(CaptureDeviceInput cdi, MediaFile file)\n+  {\n+    return liveFileMap.put(cdi, file);\n+  }\n+\n   public static MediaFile getLiveBufferedMediaFileForInput(CaptureDeviceInput cdi)\n   {\n     return liveBuffFileMap.get(cdi);\n   }\n \n+  static MediaFile putLiveBufferedMediaFileForInput(CaptureDeviceInput cdi, MediaFile file)\n+  {\n+    return liveBuffFileMap.put(cdi, file);\n+  }\n+\n   protected MediaFile(int inFileID)\n   {\n     super(inFileID);\n@@ -603,7 +613,7 @@ public class MediaFile extends DBObject implements SegmentedFile\n     Airing metaAir = null;\n     // Temporarily guess the media type based off the filename extension that\'s mapped for import (really only used for detecting DVDs)\n     String nameLC = rawFile.getName().toLowerCase();\n-    setMediaMask(Seeker.getInstance().guessImportedMediaMask(rawFile.getAbsolutePath()));\n+    setMediaMask(SeekerSelector.getInstance().guessImportedMediaMask(rawFile.getAbsolutePath()));\n \n     long rawFileLastMod = rawFile.lastModified();\n     if (rawFileLastMod == 0)\n@@ -3249,6 +3259,11 @@ public class MediaFile extends DBObject implements SegmentedFile\n     return generalType;\n   }\n \n+  int getInfoAiringID()\n+  {\n+    return infoAiringID;\n+  }\n+\n   boolean validate()\n   {\n     if (getContentAiring() == null)\n@@ -3356,6 +3371,7 @@ public class MediaFile extends DBObject implements SegmentedFile\n           if (idx != -1)\n           {\n             theShow.title.name = filePath.substring(idx, idx + theShow.title.name.length());\n+            theShow.title.ignoreCaseHash = theShow.title.name.toLowerCase().hashCode();\n           }\n         }\n         String showEp = theShow.getEpisodeName();\n@@ -3711,7 +3727,82 @@ public class MediaFile extends DBObject implements SegmentedFile\n     }\n   }\n \n+  /**\n+   * Returns a new file name that can be guaranteed to be able to be used on this media file shortly.\n+   * <p/>\n+   * You can only get one of these at a time. You must pass the {@link NextSegmentGuarantee}\n+   * returned from here to {@link #rollGuaranteedSegment(NextSegmentGuarantee)} to start a\n+   * new segment before you can call this and get a new segment.\n+   * <p/>\n+   * This is only to be used when recording and transitioning between file segments during live\n+   * playback.\n+   *\n+   * @return <code>null</code> if your request was denied.\n+   */\n+  synchronized NextSegmentGuarantee startGuaranteedSegment(long currTime)\n+  {\n+    // We can only assure one of these at a time.\n+    if (nextSegmentGuarantee != null)\n+      return null;\n+    File nextFileName = getNextSegment(false);\n+    return nextFileName != null ? nextSegmentGuarantee = new NextSegmentGuarantee(nextFileName, currTime) : null;\n+  }\n+\n+  /**\n+   * Start a new segment with the file from a {@link NextSegmentGuarantee} object.\n+   * <p/>\n+   * This is only to be used when recording and transitioning between file segments during live\n+   * playback.\n+   *\n+   * @param nextSegment The {@link NextSegmentGuarantee} object provided by\n+   *                    {@link #startGuaranteedSegment(long)} for this {@link MediaFile}.\n+   * @return <code>true</code> if the segment change was successful.\n+   */\n+  synchronized boolean rollGuaranteedSegment(NextSegmentGuarantee nextSegment)\n+  {\n+    if (nextSegmentGuarantee == null || nextSegment != nextSegmentGuarantee)\n+      return false;\n+\n+    Wizard wiz = Wizard.getInstance();\n+    try\n+    {\n+      wiz.acquireWriteLock(Wizard.MEDIAFILE_CODE);\n+      endSegment(nextSegmentGuarantee.getStartTime());\n+      startSegment(nextSegmentGuarantee.getStartTime() + 1, null, nextSegment.getNewSegment());\n+    } finally {\n+      wiz.releaseWriteLock(Wizard.MEDIAFILE_CODE);\n+    }\n+\n+    if (Sage.DBG) System.out.println("MediaFile transitioned to subfile:" + nextSegmentGuarantee.getNextSegment() + " " + this);\n+    nextSegmentGuarantee = null;\n+    return true;\n+  }\n+\n+  /**\n+   * Release a guaranteed new file segment.\n+   * <p/>\n+   * You cannot release the guarantee without the correct {@link NextSegmentGuarantee} for this\n+   * {@link MediaFile} unless the <code>force</code> parameter is get to <code>true</code>.\n+   * <p/>\n+   * This is only to be used when recording and transitioning between file segments during live\n+   * playback.\n+   *\n+   * @param nextSegment The {@link NextSegmentGuarantee} object provided by\n+   *                    {@link #startGuaranteedSegment(long)} for this {@link MediaFile}.\n+   * @param force Set <code>true</code> to clear the segment regardless of if nextSegment is correct.\n+   */\n+  synchronized void releaseGuaranteedSegment(NextSegmentGuarantee nextSegment, boolean force)\n+  {\n+    if (nextSegment == nextSegmentGuarantee || force)\n+      nextSegmentGuarantee = null;\n+  }\n+\n   synchronized void startSegment(long startTime, String newEncodedBy)\n+  {\n+    startSegment(startTime, newEncodedBy, null);\n+  }\n+\n+  private synchronized void startSegment(long startTime, String newEncodedBy, File guaranteedFile)\n   {\n     Wizard wiz = Wizard.getInstance();\n     try {\n@@ -3726,7 +3817,10 @@ public class MediaFile extends DBObject implements SegmentedFile\n       else if (newEncodedBy != null)\n         encodedBy = newEncodedBy;\n       addTimes(startTime, 0);\n-      files.addElement(getNextSegment(false));\n+      if (guaranteedFile != null)\n+        files.addElement(guaranteedFile);\n+      else\n+        files.addElement(getNextSegment(false));\n       if (isLiveBufferedStream())\n       {\n         files.firstElement().deleteOnExit();\n@@ -3738,7 +3832,7 @@ public class MediaFile extends DBObject implements SegmentedFile\n             if (Sage.DBG) System.out.println("Deleting old media file buffer file because its name changed:"  + oldFiles.get(i));\n             // If the deletion fails, be sure we don\'t import it\n             if (!oldFiles.get(i).delete())\n-              Seeker.getInstance().addIgnoreFile(oldFiles.get(i));\n+              SeekerSelector.getInstance().addIgnoreFile(oldFiles.get(i));\n           }\n         }\n         // This\'ll affect the sorting of the secondary index so we need to re-sort this in the DB\n@@ -4213,7 +4307,8 @@ public class MediaFile extends DBObject implements SegmentedFile\n       }\n \n       fileNum++;\n-    } while (!ignoreFiles && (rv.isFile() || files.contains(rv)));\n+    } while (!ignoreFiles && (rv.isFile() || files.contains(rv) ||\n+      (nextSegmentGuarantee != null && rv.equals(nextSegmentGuarantee.getNewSegment()))));\n     if (!ignoreFiles && !rv.isFile())\n     {\n       try\n@@ -4298,6 +4393,9 @@ public class MediaFile extends DBObject implements SegmentedFile\n             getContentAiring().getDuration())- LOSS_FOR_COMPLETE)));\n   }\n \n+  /**\n+   * Use with care.\n+   */\n   void thisIsComplete()\n   {\n     if (!forcedComplete)\n@@ -6143,6 +6241,12 @@ public class MediaFile extends DBObject implements SegmentedFile\n     times = newTimes;\n   }\n \n+  public String getVideoDirectory()\n+  {\n+    return videoDirectory;\n+  }\n+\n+\n   Airing myAiring;\n   String videoDirectory;\n   String name;\n@@ -6177,6 +6281,11 @@ public class MediaFile extends DBObject implements SegmentedFile\n   // This is null if we haven\'t parsed it yet or can\'t figure it out\n   ContainerFormat fileFormat;\n \n+  // This is null if we have not guaranteed anyone what the next segment file name will be. This is\n+  // the object returned when we need the next segment name before we actually want to add a new\n+  // segment to the MediaFile.\n+  private NextSegmentGuarantee nextSegmentGuarantee;\n+\n   public static final Comparator<MediaFile> AIRING_ID_COMPARATOR =\n       new Comparator<MediaFile>()\n   {\n@@ -6383,4 +6492,31 @@ public class MediaFile extends DBObject implements SegmentedFile\n       return null;\n     }\n   }\n+\n+  public class NextSegmentGuarantee\n+  {\n+    private final File nextSegment;\n+    private final long currTime;\n+\n+    private NextSegmentGuarantee(File nextSegment, long currTime)\n+    {\n+      this.nextSegment = nextSegment;\n+      this.currTime = currTime;\n+    }\n+\n+    public File getNewSegment()\n+    {\n+      return nextSegment;\n+    }\n+\n+    public String getNextSegment()\n+    {\n+      return nextSegment.toString();\n+    }\n+\n+    public long getStartTime()\n+    {\n+      return currTime;\n+    }\n+  }\n }\ndiff --git a/java/sage/MiniPlayer.java b/java/sage/MiniPlayer.java\nindex 0ebd2330..e2501dc3 100644\n--- a/java/sage/MiniPlayer.java\n+++ b/java/sage/MiniPlayer.java\n@@ -1792,10 +1792,10 @@ public class MiniPlayer implements DVDMediaPlayer\n                 if (rpSrc.isServerEOS())\n                 {\n                   MediaFile currRecFile = null;\n-                  if (uiMgr != null && (currRecFile = Seeker.getInstance().getCurrRecordFileForClient(uiMgr, false)) != null)\n+                  if (uiMgr != null && (currRecFile = SeekerSelector.getInstance().getCurrRecordFileForClient(uiMgr, false)) != null)\n                   {\n                     // Also make sure this device supports fast mux switching\n-                    CaptureDevice recInput = Seeker.getInstance().getCaptureDeviceControlledByClient(uiMgr);\n+                    CaptureDevice recInput = SeekerSelector.getInstance().getCaptureDeviceControlledByClient(uiMgr);\n                     if (!eos && (recInput == null || recInput.supportsFastMuxSwitch()))\n                     {\n                       if (Sage.DBG) System.out.println("SERVER Buffer size is now ZERO! Trigger local EOS to start the seamless file switch");\n@@ -1957,10 +1957,10 @@ public class MiniPlayer implements DVDMediaPlayer\n               if (readBufferSize <= 0)\n               {\n                 MediaFile currRecFile = null;\n-                if (uiMgr != null && (currRecFile = Seeker.getInstance().getCurrRecordFileForClient(uiMgr, false)) != null)\n+                if (uiMgr != null && (currRecFile = SeekerSelector.getInstance().getCurrRecordFileForClient(uiMgr, false)) != null)\n                 {\n                   // Also make sure this device supports fast mux switching\n-                  CaptureDevice recInput = Seeker.getInstance().getCaptureDeviceControlledByClient(uiMgr);\n+                  CaptureDevice recInput = SeekerSelector.getInstance().getCaptureDeviceControlledByClient(uiMgr);\n                   if (!eos && (recInput == null || recInput.supportsFastMuxSwitch()))\n                   {\n                     if (Sage.DBG) System.out.println("SERVER Buffer size is now ZERO! Trigger local EOS to start the seamless file switch");\ndiff --git a/java/sage/Ministry.java b/java/sage/Ministry.java\nindex c1fa10a1..7e904a7d 100644\n--- a/java/sage/Ministry.java\n+++ b/java/sage/Ministry.java\n@@ -342,7 +342,7 @@ public class Ministry implements Runnable\n           }\n         }\n \n-        Seeker seek = Seeker.getInstance();\n+        Hunter seek = SeekerSelector.getInstance();\n         for (int i = 0; i < waitingForAbsolution.size(); i++)\n         {\n           TranscodeJob tj = (TranscodeJob) waitingForAbsolution.get(i);\n@@ -396,7 +396,7 @@ public class Ministry implements Runnable\n               }\n             }\n             else\n-              Seeker.getInstance().processFileExport(tj.getMediaFile().getFiles(), MediaFile.ACQUISITION_MANUAL);\n+              SeekerSelector.getInstance().processFileExport(tj.getMediaFile().getFiles(), MediaFile.ACQUISITION_MANUAL);\n           }\n           else\n           {\n@@ -454,7 +454,7 @@ public class Ministry implements Runnable\n                   addedFile.addSegmentFileDirect((java.io.File) actualFilesVec.get(x));\n                 }\n                 ok = true;\n-                Seeker.getInstance().processFileExport(addedFile.getFiles(), MediaFile.ACQUISITION_MANUAL);\n+                SeekerSelector.getInstance().processFileExport(addedFile.getFiles(), MediaFile.ACQUISITION_MANUAL);\n               }\n               else\n                 ok = false;\ndiff --git a/java/sage/NetworkClient.java b/java/sage/NetworkClient.java\nindex 420d76cc..6d0b760a 100644\n--- a/java/sage/NetworkClient.java\n+++ b/java/sage/NetworkClient.java\n@@ -253,7 +253,7 @@ public class NetworkClient\n         }\n \n         // In case there\'s network encoders on the new client that just came up also\n-        Scheduler.getInstance().kick(true);\n+        SchedulerSelector.getInstance().kick(true);\n       }\n       else\n       {\n@@ -408,7 +408,7 @@ public class NetworkClient\n       {\n         java.util.Iterator uiWalker = UIManager.getUIIterator();\n         while (uiWalker.hasNext())\n-          Seeker.getInstance().finishWatch((UIClient) uiWalker.next());\n+          SeekerSelector.getInstance().finishWatch((UIClient) uiWalker.next());\n       }\n     }\n     if (clientListener != null)\n@@ -450,7 +450,7 @@ public class NetworkClient\n         }\n       }\n       // Trigger cleanup of the watch file map\n-      Seeker.getInstance().kick();\n+      SeekerSelector.getInstance().kick();\n     }\n   }\n \ndiff --git a/java/sage/NewStorageDeviceDetector.java b/java/sage/NewStorageDeviceDetector.java\nindex 0ba17873..57a1fab9 100644\n--- a/java/sage/NewStorageDeviceDetector.java\n+++ b/java/sage/NewStorageDeviceDetector.java\n@@ -116,10 +116,10 @@ public class NewStorageDeviceDetector implements Runnable\n         for (int i = 0; i < seekerNotifies.size(); i++)\n         {\n           Object[] currInfo = (Object[]) seekerNotifies.get(i);\n-          if (!Sage.client && Seeker.getInstance().isAutoImportEnabled() && UIManager.getNonLocalUICount() == 0)\n+          if (!Sage.client && SeekerSelector.getInstance().isAutoImportEnabled() && UIManager.getNonLocalUICount() == 0)\n           {\n             if (Sage.DBG) System.out.println("Automatically adding USB device to import path since no UI is available for prompting...");\n-            Seeker.getInstance().addArchiveDirectory(currInfo[0].toString(), Seeker.ALL_DIR_MASK);\n+            SeekerSelector.getInstance().addArchiveDirectory(currInfo[0].toString(), Seeker.ALL_DIR_MASK);\n           }\n           Catbert.distributeHookToLocalUIs("StorageDeviceAdded", new Object[] { currInfo[0], currInfo[1] });\n         }\n@@ -184,7 +184,7 @@ public class NewStorageDeviceDetector implements Runnable\n         if (Sage.DBG) System.out.println("Drives have been removed...rescan...");\n         if (!Sage.client)\n         {\n-          Seeker.getInstance().scanLibrary(false);\n+          SeekerSelector.getInstance().scanLibrary(false);\n         }\n       }\n       existingDrives = currDrivesClone;\n@@ -328,10 +328,10 @@ public class NewStorageDeviceDetector implements Runnable\n           }\n           else\n           {\n-            if (!Sage.client && Seeker.getInstance().isAutoImportEnabled() && UIManager.getNonLocalUICount() == 0)\n+            if (!Sage.client && SeekerSelector.getInstance().isAutoImportEnabled() && UIManager.getNonLocalUICount() == 0)\n             {\n               if (Sage.DBG) System.out.println("Automatically adding USB device to import path since no UI is available for prompting...");\n-              Seeker.getInstance().addArchiveDirectory(newDir.toString(), Seeker.ALL_DIR_MASK);\n+              SeekerSelector.getInstance().addArchiveDirectory(newDir.toString(), Seeker.ALL_DIR_MASK);\n             }\n             Catbert.distributeHookToLocalUIs("StorageDeviceAdded", new Object[] { newDir, specialName });\n           }\n@@ -502,8 +502,8 @@ public class NewStorageDeviceDetector implements Runnable\n       if (!mountedDevs.isEmpty())\n       {\n         if (Sage.DBG) System.out.println("External drives lost: " + mountedDevs);\n-        java.util.Set allArchiveDirs = (Seeker.getInstance().isAutoImportEnabled() && !Sage.client) ?\n-            new java.util.HashSet(java.util.Arrays.asList(Seeker.getInstance().getArchiveDirectories(Seeker.ALL_DIR_MASK))) : null;\n+        java.util.Set allArchiveDirs = (SeekerSelector.getInstance().isAutoImportEnabled() && !Sage.client) ?\n+            new java.util.HashSet(java.util.Arrays.asList(SeekerSelector.getInstance().getArchiveDirectories(Seeker.ALL_DIR_MASK))) : null;\n             java.util.Iterator walker = mountedDevs.iterator();\n             boolean validRemoves = false;\n             while (walker.hasNext())\n@@ -532,7 +532,7 @@ public class NewStorageDeviceDetector implements Runnable\n                 mountMap.remove(name);\n                 if (allArchiveDirs != null && mountedDir != null && allArchiveDirs.contains(mountedDir))\n                 {\n-                  Seeker.getInstance().removeArchiveDirectory(mountedDir, Seeker.ALL_DIR_MASK);\n+                  SeekerSelector.getInstance().removeArchiveDirectory(mountedDir, Seeker.ALL_DIR_MASK);\n                 }\n               }\n             }\n@@ -541,7 +541,7 @@ public class NewStorageDeviceDetector implements Runnable\n               if (!Sage.client)\n               {\n                 LinuxUtils.updateSmbConfig(true);\n-                Seeker.getInstance().scanLibrary(false);\n+                SeekerSelector.getInstance().scanLibrary(false);\n               }\n               extraMountCheck = true; // in case we unmounted something that changed device ID\n             }\n@@ -585,7 +585,7 @@ public class NewStorageDeviceDetector implements Runnable\n         if (Sage.DBG) System.out.println("Drives have been removed...rescan...");\n         if (!Sage.client)\n         {\n-          Seeker.getInstance().scanLibrary(false);\n+          SeekerSelector.getInstance().scanLibrary(false);\n         }\n       }\n       existingDrives = currDrivesClone;\ndiff --git a/java/sage/Person.java b/java/sage/Person.java\nindex 9b6a5a0c..1fdcb413 100644\n--- a/java/sage/Person.java\n+++ b/java/sage/Person.java\n@@ -48,6 +48,7 @@ public class Person extends DBObject\n   {\n     super(in, ver, idMap);\n     name = in.readUTF();\n+    ignoreCaseHash = name.toLowerCase().hashCode();\n     if (ver >= 0x4F)\n     {\n       Wizard wiz = Wizard.getInstance();\n@@ -145,6 +146,7 @@ public class Person extends DBObject\n   {\n     Person fromMe = (Person) x;\n     name = fromMe.name;\n+    ignoreCaseHash = name.toLowerCase().hashCode();\n     extID = fromMe.extID;\n     dateOfBirth = fromMe.dateOfBirth;\n     dateOfDeath = fromMe.dateOfDeath;\n@@ -251,6 +253,15 @@ public class Person extends DBObject\n \n   public String getName() { return name; }\n \n+  public boolean equalsIgnoreCase(Person compare)\n+  {\n+    // The hash could be 0, but it\'s not going to be any more often than any other hash and in case\n+    // we miss a case where the title is set, this covers that problem.\n+    if (ignoreCaseHash == 0 || compare.ignoreCaseHash == 0)\n+      return toString().equalsIgnoreCase(compare.toString());\n+    return compare.ignoreCaseHash == ignoreCaseHash && toString().equalsIgnoreCase(compare.toString());\n+  }\n+\n   public int getNumAwards()\n   {\n     if (extID < 0)\n@@ -387,6 +398,8 @@ public class Person extends DBObject\n   byte[][] headshotUrls;\n   // For resolved aliases\n   transient Person orgPerson;\n+  // To speed up case insensitive searches\n+  transient int ignoreCaseHash;\n \n   public static final Comparator<Person> NAME_COMPARATOR =\n       new Comparator<Person>()\ndiff --git a/java/sage/Playlist.java b/java/sage/Playlist.java\nindex 1f866588..4c2ef936 100644\n--- a/java/sage/Playlist.java\n+++ b/java/sage/Playlist.java\n@@ -931,7 +931,7 @@ public class Playlist extends DBObject\n     if (playlistItems.size() == 0)\n     {\n       if (Sage.DBG) System.out.println("Ignoring playlist due to zero size: " + theFile);\n-      Seeker.getInstance().addIgnoreFile(theFile);\n+      SeekerSelector.getInstance().addIgnoreFile(theFile);\n       return null;\n     }\n     Playlist rv;\ndiff --git a/java/sage/Pooler.java b/java/sage/Pooler.java\nindex 15f05402..67ca9c26 100644\n--- a/java/sage/Pooler.java\n+++ b/java/sage/Pooler.java\n@@ -39,6 +39,7 @@ public class Pooler\n   public static final Show[] EMPTY_SHOW_ARRAY = new Show[0];\n   public static final tv.sage.mod.AbstractWidget[] EMPTY_ABSTRACTWIDGET_ARRAY = new tv.sage.mod.AbstractWidget[0];\n   public static final Channel[] EMPTY_CHANNEL_ARRAY = new Channel[0];\n+  public static final MediaFile[] EMPTY_MEDIA_FILE_ARRAY = new MediaFile[0];\n   private static java.util.Stack vectorPool = new java.util.Stack();\n   private static int numVecsCreated = 0;\n   private static java.util.Stack arrayListPool = new java.util.Stack();\ndiff --git a/java/sage/Ripper.java b/java/sage/Ripper.java\nindex 741b90e7..4045666c 100644\n--- a/java/sage/Ripper.java\n+++ b/java/sage/Ripper.java\n@@ -128,7 +128,7 @@ public class Ripper extends SystemTask\n       if (Sage.DBG) System.out.println("CD BURNING IS COMPLETE!!!");\n       succeeded();\n \n-      Seeker.getInstance().scanLibrary(false);\n+      SeekerSelector.getInstance().scanLibrary(false);\n \n       // Now delete our temporary burn image\n       clearScratch();\ndiff --git a/java/sage/Sage.java b/java/sage/Sage.java\nindex 2bdabe04..fc769d1c 100644\n--- a/java/sage/Sage.java\n+++ b/java/sage/Sage.java\n@@ -580,7 +580,7 @@ public final class Sage\n   }\n   public static String getFileSystemTypeX(String root, boolean skipSeekerSMBCheck)\n   {\n-    if (!skipSeekerSMBCheck && Sage.LINUX_OS && Sage.EMBEDDED && Seeker.getInstance().isSmbMountedFolder(new File(root)))\n+    if (!skipSeekerSMBCheck && Sage.LINUX_OS && Sage.EMBEDDED && SeekerSelector.getInstance().isSmbMountedFolder(new File(root)))\n     {\n       return "SMB"; // for performance related problems with SMB type detection on embedded\n     }\n@@ -1398,6 +1398,8 @@ public final class Sage\n \n     // The role names are in a static array we need to fix\n     System.arraycopy(Show.getRoleNames(), 0, Show.ROLE_NAMES, 0, Show.ROLE_NAMES.length);\n+    // The localized string for checking if the category is a movie needs to be updated.\n+    Show.movieString = Sage.rez("Movie");\n \n     UserEvent.updateNameMaps();\n \ndiff --git a/java/sage/SageTV.java b/java/sage/SageTV.java\nindex dc735bd5..f2f4a1f7 100644\n--- a/java/sage/SageTV.java\n+++ b/java/sage/SageTV.java\n@@ -506,7 +506,7 @@ public class SageTV implements Runnable\n \n     //\t\tSystem.gc();\n     Sage.setSplashText(Sage.rez("Module_Init", new Object[] { Sage.rez("SageTV_Core") }));\n-    mySeeker = Seeker.prime();\n+    mySeeker = SeekerSelector.prime();\n     //\t\tSystem.gc();\n \n     if (SageConstants.LIBRARY_FUNCTION)\n@@ -618,7 +618,7 @@ public class SageTV implements Runnable\n \n     if (SageConstants.PVR)\n     {\n-      t = new Thread(myScheduler = Scheduler.getInstance(), "Scheduler");\n+      t = new Thread(myScheduler = SchedulerSelector.getInstance(), "Scheduler");\n       t.setDaemon(true);\n       t.setPriority(Thread.MIN_PRIORITY);\n       t.start();\n@@ -1245,8 +1245,8 @@ public class SageTV implements Runnable\n         }\n         else\n         {\n-          Scheduler.getInstance().kick(false);\n-          Seeker.getInstance().kick();\n+          SchedulerSelector.getInstance().kick(false);\n+          SeekerSelector.getInstance().kick();\n         }\n         java.util.Iterator walker = UIManager.getUIIterator();\n         while (walker.hasNext())\n@@ -1262,8 +1262,8 @@ public class SageTV implements Runnable\n           ((UIManager) walker.next()).deepSleep(x);\n         if (!Sage.client)\n         {\n-          Seeker.getInstance().releaseAllEncoding();\n-          Scheduler.getInstance().prepareForStandby();\n+          SeekerSelector.getInstance().releaseAllEncoding();\n+          SchedulerSelector.getInstance().prepareForStandby();\n           // Also release all IR xmt devices\n           ExternalTuningManager.goodbye();\n         }\n@@ -1491,8 +1491,8 @@ public class SageTV implements Runnable\n   private static final Object quantaLock = new Object();\n \n   private static EPG guide;\n-  private static Seeker mySeeker;\n-  private static Scheduler myScheduler;\n+  private static Hunter mySeeker;\n+  private static SchedulerInterface myScheduler;\n   private static Carny god;\n   private static MMC mmc;\n   private static boolean alive;\ndiff --git a/java/sage/SageTVConnection.java b/java/sage/SageTVConnection.java\nindex 6f9b7392..0dc91796 100644\n--- a/java/sage/SageTVConnection.java\n+++ b/java/sage/SageTVConnection.java\n@@ -15,8 +15,6 @@\n  */\n package sage;\n \n-import sage.plugin.PluginEventManager;\n-\n import java.util.Map.Entry;\n import java.util.Set;\n \n@@ -999,7 +997,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     if (theAir != null)\n     {\n       int[] watchError = new int[1];\n-      watchFile = Seeker.getInstance().requestWatch(theAir, watchError, requestor);\n+      watchFile = SeekerSelector.getInstance().requestWatch(theAir, watchError, requestor);\n       if (watchFile != null)\n         rv = 0;\n       else\n@@ -1024,7 +1022,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     if (mf != null)\n     {\n       int[] watchError = new int[1];\n-      watchFile = Seeker.getInstance().requestWatch(mf, watchError, requestor);\n+      watchFile = SeekerSelector.getInstance().requestWatch(mf, watchError, requestor);\n       if (watchFile != null)\n         rv = 0;\n       else\n@@ -1042,7 +1040,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n   {\n     java.io.DataInputStream dis = new java.io.DataInputStream(new java.io.ByteArrayInputStream(myMsg.data));\n     UIClient requestor = new RemoteUI(dis.readUTF());\n-    Seeker.getInstance().finishWatch(requestor);\n+    SeekerSelector.getInstance().finishWatch(requestor);\n     return new Msg(RESPONSE_MSG, myMsg.type, null, myMsg.id);\n   }\n \n@@ -1052,7 +1050,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     UIClient requestor = new RemoteUI(dis.readUTF());\n     String chanString = dis.readUTF();\n     String mmcInputName = dis.readUTF();\n-    int rv = Seeker.getInstance().forceChannelTune(mmcInputName, chanString, requestor);\n+    int rv = SeekerSelector.getInstance().forceChannelTune(mmcInputName, chanString, requestor);\n     byte[] data = new byte[4];\n     writeIntBytes(rv, data, 0);\n     return new Msg(RESPONSE_MSG, myMsg.type, data, myMsg.id);\n@@ -1110,22 +1108,22 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n \n     if (totalFileSize > freeSpace)\n     {\n-      if (!Sage.WINDOWS_OS || Seeker.getInstance().isPathInManagedStorage(destFile))\n+      if (!Sage.WINDOWS_OS || SeekerSelector.getInstance().isPathInManagedStorage(destFile))\n       {\n         // Make a storage request with Seeker to get the space we need, and then see if we\'ve got the space\n         // now and then cancel the request.\n         if (Sage.DBG) System.out.println("Requesting Seeker to clear up " + ((totalFileSize - freeSpace)/1000000) + "MB worth of space");\n-        java.io.File tempFile = Seeker.getInstance().requestDirectoryStorage("scratch", totalFileSize - freeSpace);\n-        synchronized (Seeker.getInstance())\n+        java.io.File tempFile = SeekerSelector.getInstance().requestDirectoryStorage("scratch", totalFileSize - freeSpace);\n+        synchronized (SeekerSelector.getInstance())\n         {\n-          Seeker.getInstance().kick();\n+          SeekerSelector.getInstance().kick();\n           try\n           {\n-            Seeker.getInstance().wait(5000);\n+            SeekerSelector.getInstance().wait(5000);\n           }catch (InterruptedException e){}\n         }\n         freeSpace = Sage.getDiskFreeSpace(destFile.getAbsolutePath());\n-        Seeker.getInstance().clearDirectoryStorageRequest(tempFile);\n+        SeekerSelector.getInstance().clearDirectoryStorageRequest(tempFile);\n         if (totalFileSize > freeSpace)\n         {\n           System.out.println("ERROR Unable to clear up enough free space for library import");\n@@ -1177,7 +1175,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();\n     java.io.DataOutputStream dos = new java.io.DataOutputStream(baos);\n \n-    Object[] schedAirs = Seeker.getInstance().getScheduledAirings();\n+    Object[] schedAirs = SeekerSelector.getInstance().getScheduledAirings();\n     dos.writeInt(schedAirs.length);\n     for (int i = 0; i < schedAirs.length; i++)\n     {\n@@ -1193,7 +1191,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n \n   private Msg recvGetCurrRecordFiles(Msg myMsg) throws java.io.IOException\n   {\n-    MediaFile[] mfs = Seeker.getInstance().getCurrRecordFiles();\n+    MediaFile[] mfs = SeekerSelector.getInstance().getCurrRecordFiles();\n     byte[] data = new byte[4*(mfs.length + 1)];\n     int offset = 0;\n     writeIntBytes(mfs.length, data, 0);\n@@ -1210,7 +1208,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n   {\n     java.io.DataInputStream dis = new java.io.DataInputStream(new java.io.ByteArrayInputStream(myMsg.data));\n     UIClient requestor = new RemoteUI(dis.readUTF());\n-    MediaFile mf = Seeker.getInstance().getCurrRecordFileForClient(requestor);\n+    MediaFile mf = SeekerSelector.getInstance().getCurrRecordFileForClient(requestor);\n     byte[] data = new byte[4];\n     writeIntBytes((mf == null) ? 0 : mf.id, data, 0);\n     return new Msg(RESPONSE_MSG, myMsg.type, data, myMsg.id);\n@@ -1219,7 +1217,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n   // This is a broadcast message, no response is needed\n   private void recvScheduleChanged(Msg myMsg) throws java.io.IOException\n   {\n-    Scheduler.getInstance().setClientDontKnowFlag(myMsg.data[0] != 0);\n+    SchedulerSelector.getInstance().setClientDontKnowFlag(myMsg.data[0] != 0);\n     Catbert.distributeHookToLocalUIs("RecordingScheduleChanged", null);\n \n     // Not the most efficient way in the world, but much easier than the other options\n@@ -1278,7 +1276,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     int airID = convertToLocalDBID(dis.readInt());\n     Airing theAir = Wizard.getInstance().getAiringForID(airID);\n     if (theAir != null)\n-      rv = Seeker.getInstance().record(theAir, requestor);\n+      rv = SeekerSelector.getInstance().record(theAir, requestor);\n     else\n       rv = VideoFrame.WATCH_FAILED_NULL_AIRING;\n     byte[] data = new byte[4];\n@@ -1295,7 +1293,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     Airing theAir = Wizard.getInstance().getAiringForID(airID);\n     if (theAir != null)\n     {\n-      Seeker.getInstance().cancelRecord(theAir, requestor);\n+      SeekerSelector.getInstance().cancelRecord(theAir, requestor);\n       rv = true;\n     }\n     else\n@@ -1319,11 +1317,11 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     int rv;\n     if (mr != null)\n     {\n-      rv = Seeker.getInstance().modifyRecord(startTime-mr.startTime, endTime-mr.getEndTime(), mr, requestor);\n+      rv = SeekerSelector.getInstance().modifyRecord(startTime-mr.startTime, endTime-mr.getEndTime(), mr, requestor);\n     }\n     else\n     {\n-      rv = Seeker.getInstance().timedRecord(startTime, endTime, stationID, recurCode, air, requestor);\n+      rv = SeekerSelector.getInstance().timedRecord(startTime, endTime, stationID, recurCode, air, requestor);\n     }\n     byte[] data = new byte[4];\n     writeIntBytes(rv, data, 0);\n@@ -2992,7 +2990,7 @@ public class SageTVConnection implements Runnable, Wizard.XctSyncClient, Carny.P\n     try\n     {\n       byte[] data = new byte[1];\n-      data[0] = (byte) (Scheduler.getInstance().areThereDontKnows() ? 1 : 0);\n+      data[0] = (byte) (SchedulerSelector.getInstance().areThereDontKnows() ? 1 : 0);\n       postMessage(new Msg(BROADCAST_MSG, SCHEDULE_CHANGED, data));\n     }\n     catch (Exception e)\ndiff --git a/java/sage/Scheduler.java b/java/sage/Scheduler.java\nindex 0217bc40..62934c1d 100644\n--- a/java/sage/Scheduler.java\n+++ b/java/sage/Scheduler.java\n@@ -33,7 +33,7 @@ import java.util.SortedSet;\n import java.util.TreeSet;\n import java.util.Vector;\n \n-public class Scheduler implements Runnable\n+public class Scheduler implements SchedulerInterface\n {\n   public static final long SCHEDULING_LOOKBEHIND = 180000L;// 3 min\n   private static final float FUZZIFIER = 0.05f;//0.1f;\n@@ -70,7 +70,7 @@ public class Scheduler implements Runnable\n     prepped = false;\n   }\n \n-  boolean isPrepped() { return prepped; }\n+  public boolean isPrepped() { return prepped; }\n \n   private void addToDontSchedule(Airing addMe)\n   {\n@@ -372,7 +372,7 @@ public class Scheduler implements Runnable\n     }\n   }\n \n-  void setClientDontKnowFlag(boolean x) { clientDontKnowFlag = x; }\n+  public void setClientDontKnowFlag(boolean x) { clientDontKnowFlag = x; }\n \n   public boolean areThereDontKnows()\n   {\n@@ -465,7 +465,7 @@ public class Scheduler implements Runnable\n     return es == null ? new Vector<Airing>() : es.pubMustSee;\n   }\n \n-  void goodbye()\n+  public void goodbye()\n   {\n     alive = false;\n     synchronized (this)\n@@ -904,8 +904,8 @@ public class Scheduler implements Runnable\n     }\n \n     // Create a list of the ES\'s thats sorted by merit\n-    CaptureDevice[] sortedEncNames = encoderScheduleMap.keySet().toArray(new CaptureDevice[0]);\r\n-    Arrays.sort(sortedEncNames, CaptureDevice.captureDeviceSorter);\r\n+    CaptureDevice[] sortedEncNames = encoderScheduleMap.keySet().toArray(new CaptureDevice[0]);\n+    Arrays.sort(sortedEncNames, CaptureDevice.captureDeviceSorter);\n     EncoderSchedule[] sortedEncs = new EncoderSchedule[sortedEncNames.length];\n     for (int i = 0; i < sortedEncNames.length; i++)\n       sortedEncs[i] = encoderScheduleMap.get(sortedEncNames[i]);\n@@ -1121,7 +1121,7 @@ public class Scheduler implements Runnable\n         }\n         if (appendage == null && currAirOptions.isEmpty())\n         {\n-          // Unschedulable with no hopes of mutating the schedule to accomdate it\n+          // Unschedulable with no hopes of mutating the schedule to accommodate it\n           // Check for what caused it to NOT get scheduled and if there\'s any ambiguous conflicts\n           // in there, then add them to our pubUnresolvedConflicts list\n           Vector<Airing> unwantedAirs = showSetMap.get(showSetMapKey);\n@@ -1185,10 +1185,10 @@ public class Scheduler implements Runnable\n           // BUT there were conflicts with mutable schedule entries, these are the ones that conflicts\n           // with what\'s left in currAirOptions. No we need to find the conflict map of what\'s in the current\n           // schedule for each of those air options. The first one that can create a complete schedule is the\n-          // preferred one we\'ll use. We go through the progressively, if any of them CANNOT make a complete\n+          // preferred one we\'ll use. We go through them progressively, if any of them CANNOT make a complete\n           // schedule, they get removed from currAirOptions. If we go through them all and can\'t find any full\n           // schedule permutations, then we are unable to schedule this one and must add it to the pubConflicts\n-          // if its ambigious why this occurred\n+          // if its ambiguous why this occurred\n           boolean rearrangeSucceeded = false;\n           Airing currAirOpt = null;\n \n@@ -1963,7 +1963,7 @@ public class Scheduler implements Runnable\n \n     /*\n      * NOTE WE STILL NEED TO MANIPULATE THE SCHEDULE BY SWAPPING ANY ENCODERS THAT\n-     * GIVE US AN ADVANTAGE. This only effects what were recording now more or less.\n+     * GIVE US AN ADVANTAGE. This only effects what we\'re recording now more or less.\n      * The problems we can address here are unnecessary promptings for channel changes.\n      * The procedure we want to follow is that any encoders with desired encodings\n      * should try to make 2 optimizations. No optimization can be done if the nextRecord\n@@ -1971,7 +1971,7 @@ public class Scheduler implements Runnable\n      * is if there\'s another encoder scheduled to record the show that\'s on immediately after the currRecord\n      * and that show is a must see or MR, then swap that schedule entry to the forced encoder\'s schedule.\n      * --this first optimization is unnecessary because the Seeker will enforce that by default and then\n-     * the scheduler will rerun and fix itself to accomodate for that)\n+     * the scheduler will rerun and fix itself to accommodate for that)\n      * The next optimization is if there\'s a must see scheduled immediately after the currRecord (on a different station)\n      * then try and swap that next record to another encoder so we don\'t have to prompt the user\n      * unnecessarily about a channel change.\n@@ -2035,7 +2035,7 @@ public class Scheduler implements Runnable\n                     {\n                       continue swap_encoder_loop;\n                     }\n-                    // We can\'t just use straight alingment or if it\'s off by a minute we will still get prompted.\n+                    // We can\'t just use straight alignment or if it\'s off by a minute we will still get prompted.\n                     // A good example is a program that starts one minute later.\n                     // The old code was a straight ==, now we make sure the gap is bigger than 2 * the prompt advance.\n                     if ((getSchedulingEnd(sortedEncs[i].currRecord) >= getSchedulingStart(otherMustAir) - 2*askAdvance) &&\n@@ -2129,7 +2129,7 @@ public class Scheduler implements Runnable\n     {\n       potentials.sort();\n \n-      // Build the redudancy map. This saves us a LOT of CPU time once they have any kind of IR built up because of its n^2 performance\n+      // Build the redundancy map. This saves us a LOT of CPU time once they have any kind of IR built up because of its n^2 performance\n       agentFileRedMap = new HashMap<Agent, List<Airing>>();\n       agentSchedRedMap = new HashMap<Agent, List<Airing>>();\n       for (int i = 0; i < wizFileCache.length; i++)\ndiff --git a/java/sage/SchedulerInterface.java b/java/sage/SchedulerInterface.java\nnew file mode 100644\nindex 00000000..b727ba65\n--- /dev/null\n+++ b/java/sage/SchedulerInterface.java\n@@ -0,0 +1,33 @@\n+package sage;\n+\n+import java.util.Map;\n+import java.util.Vector;\n+\n+public interface SchedulerInterface extends Runnable\n+{\n+  public boolean isPrepped();\n+\n+  public long getNextMustSeeTime();\n+\n+  public void setClientDontKnowFlag(boolean x);\n+\n+  public boolean areThereDontKnows();\n+\n+  public void kick(boolean delay);\n+\n+  public void prepareForStandby();\n+\n+  public Map<DBObject, Vector<Airing>> getUnresolvedConflictsMap();\n+\n+  public Map<DBObject, Vector<Airing>> getConflictsMap();\n+\n+  public CaptureDevice[] getMyEncoders();\n+\n+  public String[] getMyEncoderNames();\n+\n+  public Vector<Airing> getSchedule(CaptureDevice capDev);\n+\n+  public Vector<Airing> getMustSee(CaptureDevice capDev);\n+\n+  public void goodbye();\n+}\ndiff --git a/java/sage/SchedulerSelector.java b/java/sage/SchedulerSelector.java\nnew file mode 100644\nindex 00000000..534467c7\n--- /dev/null\n+++ b/java/sage/SchedulerSelector.java\n@@ -0,0 +1,36 @@\n+/*\n+ * Copyright 2015 The SageTV Authors. All Rights Reserved.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *  http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an "AS IS" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+package sage;\n+\n+public class SchedulerSelector\n+{\n+  private static class SchedulerInterfaceHolder\n+  {\n+    // TODO: This will be pulled together in a later commit.\n+    private static final SchedulerInterface ourInstance =\n+      SeekerSelector.USE_BETA_SEEKER ? null /*Scheduler2.getInstance();*/ : Scheduler.getInstance();\n+  }\n+\n+  public static SchedulerInterface getInstance()\n+  {\n+    return SchedulerInterfaceHolder.ourInstance;\n+  }\n+\n+  private SchedulerSelector()\n+  {\n+\n+  }\n+}\ndiff --git a/java/sage/Seeker.java b/java/sage/Seeker.java\nindex c225b925..784a845a 100644\n--- a/java/sage/Seeker.java\n+++ b/java/sage/Seeker.java\n@@ -48,7 +48,7 @@ import java.util.Vector;\n  * VideoSetupMenu for modifying video directory, EventRouter for determining if Power\n  * button turns on or off\n  */\n-public class Seeker implements Runnable\n+public class Seeker implements Hunter\n {\n   private static final String VIDEO_STORAGE = "video_storage";\n   private static final String ARCHIVE_DIRECTORY = "archive_directory";\n@@ -1707,7 +1707,7 @@ public class Seeker implements Runnable\n     }\n   }\n \n-  void kick()\n+  public void kick()\n   {\n     synchronized (this)\n     {\n@@ -4568,7 +4568,7 @@ if (encState.currRecord.getDuration() + (Sage.time() - encState.lastResetTime) >\n     return wakeupTime;\n   }\n \n-  void goodbye()\n+  public void goodbye()\n   {\n     //Set savedPartials = new HashSet(currRecordFiles);\n     // NOTE: NARFLEX - 02/28/08 - This needs to be synchronized on the Seeker because the VideoFrame will\n@@ -5050,7 +5050,7 @@ if (encState.currRecord.getDuration() + (Sage.time() - encState.lastResetTime) >\n     }\n   }\n \n-  int forceChannelTune(String mmcInputName, String chanString, UIClient uiClient)\n+  public int forceChannelTune(String mmcInputName, String chanString, UIClient uiClient)\n   {\n     if (Sage.client)\n     {\n@@ -5138,7 +5138,7 @@ if (encState.currRecord.getDuration() + (Sage.time() - encState.lastResetTime) >\n     return null;\n   }\n \n-  MediaFile requestWatch(Airing watchAir, int[] errorReturn, UIClient uiClient)\n+  public MediaFile requestWatch(Airing watchAir, int[] errorReturn, UIClient uiClient)\n   {\n     if (Sage.client)\n       return NetworkClient.getSN().requestWatch(uiClient, watchAir, errorReturn);\ndiff --git a/java/sage/SeekerSelector.java b/java/sage/SeekerSelector.java\nnew file mode 100644\nindex 00000000..f00731a1\n--- /dev/null\n+++ b/java/sage/SeekerSelector.java\n@@ -0,0 +1,78 @@\n+/*\n+ * Copyright 2015 The SageTV Authors. All Rights Reserved.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *  http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an "AS IS" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+package sage;\n+\n+/**\n+ * Enables us to seamlessly switch between {@link Seeker} and {@link Seeker2} + {@link Library}.\n+ * <p/>\n+ * Any methods that need to be exposed for compatibility must be added to the Hunter interface. Any\n+ * new methods specific to Seeker2 or Library must not be added to the interface and must be\n+ * accessed by first checking <code>instanceof Seeker2Library</code>, then using\n+ * {@link Seeker2Library#getSeeker2()} or {@link Seeker2Library#getLibrary()}.\n+ */\n+public class SeekerSelector\n+{\n+  // Use the new splitter. This enables recordings will overlapping padding on the same channel to\n+  // be recorded on one capture device.\n+  // TODO: This will be enabled on a later commit.\n+  public static final boolean USE_BETA_SEEKER = false; //Sage.getBoolean("seeker/use_beta_seeker", false);\n+\n+  public static class HunterHolder\n+  {\n+    // TODO: This will be enabled on a later commit.\n+    private final static Hunter hunterInstance =\n+      USE_BETA_SEEKER ? null /*Seeker2Library.getInstance()*/ : Seeker.getInstance();;\n+  }\n+\n+  /**\n+   * Returns {@link Seeker2Library#getSeeker2()} or {@link Seeker2Library#getLibrary()} instance\n+   * depending on the value of {@link SeekerSelector#USE_BETA_SEEKER}.\n+   */\n+  public static Hunter getInstance()\n+  {\n+    return HunterHolder.hunterInstance;\n+  }\n+\n+  private SeekerSelector()\n+  {\n+  }\n+\n+  private static final Object instanceLock = new Object();\n+  public static Hunter prime()\n+  {\n+    Hunter instance = getInstance();\n+    synchronized (instanceLock)\n+    {\n+      // TODO: This will be enabled on a later commit.\n+      /*if (USE_BETA_SEEKER)\n+        Seeker2.prime();\n+      else*/\n+        Seeker.prime();\n+    }\n+    return instance;\n+  }\n+\n+  public static String[] getVideoDiskspaceRules()\n+  {\n+    // These return the exact same values, but we need to be consistent until/if we decide to retire\n+    // the old seeker.\n+    // TODO: This will be enabled on a later commit.\n+    /*if (USE_BETA_SEEKER)\n+      return Seeker2.getVideoDiskspaceRules();\n+    else*/\n+      return Seeker.getVideoDiskspaceRules();\n+  }\n+}\ndiff --git a/java/sage/ServerPowerManagement.java b/java/sage/ServerPowerManagement.java\nindex f7fc34f7..ac996d42 100644\n--- a/java/sage/ServerPowerManagement.java\n+++ b/java/sage/ServerPowerManagement.java\n@@ -54,7 +54,7 @@ public class ServerPowerManagement extends PowerManagement\n     // server situation\n     if (!Sage.client)\n     {\n-      if (Seeker.getInstance().requiresPower())\n+      if (SeekerSelector.getInstance().requiresPower())\n         return SYSTEM_POWER;\n       if (Ministry.getInstance().requiresPower())\n     \treturn SYSTEM_POWER;\n@@ -82,7 +82,7 @@ public class ServerPowerManagement extends PowerManagement\n   // Determine the next time we need to be awake to do a recording\n   protected long getWakeupTime()\n   {\n-    return Scheduler.getInstance().getNextMustSeeTime();\n+    return SchedulerSelector.getInstance().getNextMustSeeTime();\n   }\n \n   public long getWaitTime() { return Sage.getLong("power_management_wait_time_new", Math.min(Sage.getLong("power_management_wait_time", 30000), 10000)); }\ndiff --git a/java/sage/Show.java b/java/sage/Show.java\nindex 4eff5e6b..51099ee9 100644\n--- a/java/sage/Show.java\n+++ b/java/sage/Show.java\n@@ -133,6 +133,9 @@ public final class Show extends DBObject\n   public static final int IMAGE_SOURCE_BONES_GSTATIC = 0;\n   public static final int IMAGE_SOURCE_FIBER_GSTATIC = 1;\n \n+  // Used for localized check on isMovie() method.\n+  static String movieString = Sage.rez("Movie");\n+\n   public static String getRoleString(int x)\n   {\n     if (x < 0 || x >= ROLE_NAMES.length)\n@@ -202,6 +205,18 @@ public final class Show extends DBObject\n \n   public String getCategory() { return (categories.length == 0) ? "" : categories[0].name; }\n \n+  /**\n+   * Checks case if the provided string matches the category.\n+   *\n+   * @param compare This is a Stringer optimized lookup that requires the incoming string to already\n+   *               be all lowercase.\n+   * @return <code>true</code> if the string matches the category.\n+   */\n+  public boolean isCategory(String compare)\n+  {\n+    return categories.length != 0 && categories[0].equalsIgnoreCase(compare);\n+  }\n+\n   public String getSubCategory() { return (categories.length < 2) ? "" : categories[1].name; }\n \n   public String[] getCategories()\n@@ -261,7 +276,7 @@ public final class Show extends DBObject\n     }\n   }\n \n-  public void appendExpandedRatingsString(StringBuffer rv)\n+  public void appendExpandedRatingsString(StringBuilder rv)\n   {\n     for (int i = 0; i < ers.length; i++)\n     {\n@@ -273,7 +288,7 @@ public final class Show extends DBObject\n \n   public String getExpandedRatingsString()\n   {\n-    StringBuffer rv = new StringBuffer();\n+    StringBuilder rv = new StringBuilder();\n     appendExpandedRatingsString(rv);\n     return rv.toString();\n   }\n@@ -290,7 +305,7 @@ public final class Show extends DBObject\n     return rv;\n   }\n \n-  public void appendBonusesString(StringBuffer rv)\n+  public void appendBonusesString(StringBuilder rv)\n   {\n     for (int i = 0; i < bonuses.length; i++)\n     {\n@@ -302,7 +317,7 @@ public final class Show extends DBObject\n \n   public String getBonusesString()\n   {\n-    StringBuffer rv = new StringBuffer();\n+    StringBuilder rv = new StringBuilder();\n     appendBonusesString(rv);\n     return rv.toString();\n   }\n@@ -1464,7 +1479,7 @@ public final class Show extends DBObject\n   {\n     if (externalID.length > 1 && externalID[0] == \'M\' && externalID[1] == \'V\')\n       return true;\n-    if (categories != null && categories.length > 0 && Sage.rez("Movie").equals(categories[0].name))\n+    if (categories != null && categories.length > 0 && movieString.equals(categories[0].name))\n       return true;\n     return false;\n   }\ndiff --git a/java/sage/Stringer.java b/java/sage/Stringer.java\nindex 0ef8cae2..05992e1c 100644\n--- a/java/sage/Stringer.java\n+++ b/java/sage/Stringer.java\n@@ -36,6 +36,7 @@ public final class Stringer extends DBObject implements Comparable<Stringer>\n   {\n     super(in, ver, idMap);\n     name = in.readUTF();\n+    ignoreCaseHash = name.toLowerCase().hashCode();\n   }\n \n   void write(DataOutput out, int flags) throws IOException\n@@ -53,6 +54,7 @@ public final class Stringer extends DBObject implements Comparable<Stringer>\n   {\n     Stringer fromMe = (Stringer) x;\n     name = fromMe.name;\n+    ignoreCaseHash = name.toLowerCase().hashCode();\n     super.update(fromMe);\n   }\n \n@@ -61,7 +63,37 @@ public final class Stringer extends DBObject implements Comparable<Stringer>\n     return toString().compareTo(o.toString());\n   }\n \n+  // NOTE: Any string passed into this method must already be all lowercase.\n+  public boolean equalsIgnoreCase(String compare)\n+  {\n+    String thisName = name;\n+    int thisIgnoreHash = ignoreCaseHash;\n+    // The hash could be 0, but it\'s not going to be any more often than any other hash and in case\n+    // we miss a case where the title is set, this covers that problem.\n+    if (thisIgnoreHash == 0)\n+      return thisName.equalsIgnoreCase(compare);\n+    return compare == thisName ||\n+      (compare.hashCode() == thisIgnoreHash && compare.length() == thisName.length() &&\n+        thisName.regionMatches(true, 0, compare, 0, thisName.length()));\n+  }\n+\n+  public boolean equalsIgnoreCase(Stringer compare)\n+  {\n+    String thisName = name;\n+    String compareName = compare.name;\n+    int thisIgnoreHash = ignoreCaseHash;\n+    // The hash could be 0, but it\'s not going to be any more often than any other hash and in case\n+    // we miss a case where the title is set, this covers that problem.\n+    if (thisIgnoreHash == 0)\n+      return thisName.equalsIgnoreCase(compareName);\n+    return compareName == thisName ||\n+      (compare.ignoreCaseHash == thisIgnoreHash && compareName.length() == thisName.length() &&\n+        thisName.regionMatches(true, 0, compareName, 0, thisName.length()));\n+  }\n+\n   String name;\n+  // Used for case insensitive comparisons.\n+  transient int ignoreCaseHash;\n \n   public static final Comparator<Stringer> NAME_COMPARATOR =\n       new Comparator<Stringer>()\ndiff --git a/java/sage/SystemTask.java b/java/sage/SystemTask.java\nindex f633b069..baf89750 100644\n--- a/java/sage/SystemTask.java\n+++ b/java/sage/SystemTask.java\n@@ -88,7 +88,7 @@ public abstract class SystemTask implements Runnable\n       alive = false;\n       if (scratchDir != null)\n       {\n-        Seeker.getInstance().clearDirectoryStorageRequest(scratchDir);\n+        SeekerSelector.getInstance().clearDirectoryStorageRequest(scratchDir);\n         scratchDir = null;\n       }\n     }\n@@ -186,8 +186,8 @@ public abstract class SystemTask implements Runnable\n \n   protected void allocateScratchSpace(long size)\n   {\n-    scratchDir = Seeker.getInstance().requestDirectoryStorage("scratch", size);\n-    Seeker.getInstance().kick();\n+    scratchDir = SeekerSelector.getInstance().requestDirectoryStorage("scratch", size);\n+    SeekerSelector.getInstance().kick();\n     if (Sage.DBG) System.out.println("Temp dir for task data: " + scratchDir);\n   }\n \ndiff --git a/java/sage/TranscodeJob.java b/java/sage/TranscodeJob.java\nindex 0a092155..1b7eec73 100644\n--- a/java/sage/TranscodeJob.java\n+++ b/java/sage/TranscodeJob.java\n@@ -220,7 +220,7 @@ public abstract class TranscodeJob\n     setJobState(TRANSCODING);\n     transcodeSegment = getStartingSegment();\n     if (Sage.DBG) System.out.println("Initiating xcode for " + mf.getFile(transcodeSegment));\n-    Seeker.getInstance().requestFileStorage(getTempFile(transcodeSegment), estimateTranscodeSize());\n+    SeekerSelector.getInstance().requestFileStorage(getTempFile(transcodeSegment), estimateTranscodeSize());\n     getTempFile(transcodeSegment).deleteOnExit();\n     transcodeNow();\n   }\n@@ -230,7 +230,7 @@ public abstract class TranscodeJob\n     setJobState(TRANSCODING);\n     transcodeSegment++;\n     if (Sage.DBG) System.out.println("Initiating xcode for " + mf.getFile(transcodeSegment));\n-    Seeker.getInstance().requestFileStorage(getTempFile(transcodeSegment), estimateTranscodeSize());\n+    SeekerSelector.getInstance().requestFileStorage(getTempFile(transcodeSegment), estimateTranscodeSize());\n     getTempFile(transcodeSegment).deleteOnExit();\n     transcodeNow();\n   }\n@@ -242,7 +242,7 @@ public abstract class TranscodeJob\n   {\n     java.io.File deadFile = getTempFile(transcodeSegment);\n     if (deadFile != null)\n-      Seeker.getInstance().clearFileStorageRequest(deadFile);\n+      SeekerSelector.getInstance().clearFileStorageRequest(deadFile);\n   }\n \n   public void abandon()\ndiff --git a/java/sage/UIManager.java b/java/sage/UIManager.java\nindex 3d67ef1f..b0b4c6c8 100644\n--- a/java/sage/UIManager.java\n+++ b/java/sage/UIManager.java\n@@ -1643,7 +1643,7 @@ public class UIManager implements Runnable, UIClient\n         // to view that aren\'t being currently recorded.\n         int numEncoders = MMC.getInstance().getConfiguredInputs().length;\n         int lastStationID = vf.getBestLastStationID();\n-        MediaFile[] currFile = Seeker.getInstance().getCurrRecordFiles();\n+        MediaFile[] currFile = SeekerSelector.getInstance().getCurrRecordFiles();\n         if (Sage.DBG) System.out.println("#CurrRecs=" + currFile.length + " #Encoders=" + numEncoders);\n         for (int z = VideoFrame.getEnablePC() ? 0 : 1; z < 3; z++)\n         {\n@@ -2338,7 +2338,7 @@ public class UIManager implements Runnable, UIClient\n   {\n     if (isTaskbar)\n     {\n-      MediaFile[] currRecs = Seeker.getInstance().getCurrRecordFiles();\n+      MediaFile[] currRecs = SeekerSelector.getInstance().getCurrRecordFiles();\n       String tipString = (currRecs.length > 0 ?\n           (Sage.rez("SageTV") + "-" + Sage.rez("Recording") + " " + currRecs[0].getContentAiring().getShortString()) :\n             Sage.rez("SageTV"));\ndiff --git a/java/sage/VideoFrame.java b/java/sage/VideoFrame.java\nindex c26a049f..0e9a9152 100644\n--- a/java/sage/VideoFrame.java\n+++ b/java/sage/VideoFrame.java\n@@ -761,7 +761,7 @@ public final class VideoFrame extends BasicVideoFrame implements Runnable\n \n   public void run()\n   {\n-    seek = Seeker.getInstance();\n+    seek = SeekerSelector.getInstance();\n     if (Sage.DBG) System.out.println("VF thread is now running...");\n     long waitTime = 0;\n     long fileReadyTime = 0;\n@@ -5056,7 +5056,7 @@ public final class VideoFrame extends BasicVideoFrame implements Runnable\n           // Check if it\'s a recording with a live preview we can access\n           if (theFile.isLocalFile() && theFile.isRecording() && !Sage.client)\n           {\n-            CaptureDeviceInput cdi = Seeker.getInstance().getInputForCurrRecordingFile(theFile);\n+            CaptureDeviceInput cdi = SeekerSelector.getInstance().getInputForCurrRecordingFile(theFile);\n             if (cdi != null && cdi.getCaptureDevice().getNativeVideoPreviewConfigHandle() != 0)\n             {\n               return true;\n@@ -5160,7 +5160,7 @@ public final class VideoFrame extends BasicVideoFrame implements Runnable\n         // Check if it\'s a recording with a live preview we can access\n         if (theFile.isLocalFile() && theFile.isRecording() && !Sage.client)\n         {\n-          CaptureDeviceInput cdi = Seeker.getInstance().getInputForCurrRecordingFile(theFile);\n+          CaptureDeviceInput cdi = SeekerSelector.getInstance().getInputForCurrRecordingFile(theFile);\n           if (cdi != null && cdi.getCaptureDevice().getNativeVideoPreviewConfigHandle() != 0)\n           {\n             return new DShowSharedLiveMediaPlayer(cdi.toString());\n@@ -5763,7 +5763,7 @@ public final class VideoFrame extends BasicVideoFrame implements Runnable\n     });\n   }\n \n-  private Seeker seek;\n+  private Hunter seek;\n   private MediaFile currFile;\n   private int mediaPlayerSetup;\n   private boolean currFileLoaded;\ndiff --git a/java/sage/WarlockRipper.java b/java/sage/WarlockRipper.java\nindex 56bab94e..e681b48a 100644\n--- a/java/sage/WarlockRipper.java\n+++ b/java/sage/WarlockRipper.java\n@@ -307,7 +307,7 @@ public class WarlockRipper extends EPGDataSource\n     sb.append(\' \');\n     sb.append(wiz.getSize(Wizard.MEDIAFILE_CODE));\n     sb.append(\' \');\n-    long[] usedAndTotalSpace = Seeker.getInstance().getUsedAndTotalVideoDiskspace();\n+    long[] usedAndTotalSpace = SeekerSelector.getInstance().getUsedAndTotalVideoDiskspace();\n     sb.append(usedAndTotalSpace[1]);\n     sb.append(\' \');\n     sb.append(usedAndTotalSpace[0]);\ndiff --git a/java/sage/Wizard.java b/java/sage/Wizard.java\nindex ab28c3a9..cc963f55 100644\n--- a/java/sage/Wizard.java\n+++ b/java/sage/Wizard.java\n@@ -710,7 +710,7 @@ public class Wizard implements EPGDBPublic2\n           while (true)\n           {\n             try{Thread.sleep(5000);}catch(Exception e){}\n-            if (numUncompXcts > NUM_TRANSACTIONS_TO_COMPRESS && !Seeker.getInstance().isDoingImportScan())\n+            if (numUncompXcts > NUM_TRANSACTIONS_TO_COMPRESS && !SeekerSelector.getInstance().isDoingImportScan())\n             {\n               if (Sage.DBG) System.out.println("Setting overflow marker because we\'ve exceeded the # of xcts for a re-save: num=" + numUncompXcts);\n               flashOverflow = true;\n@@ -788,17 +788,6 @@ public class Wizard implements EPGDBPublic2\n     Sage.putInt(prefsRoot + NOSHOW_ID, noShowID = noShow.id);\n   }\n \n-  /**\n-   * Is an airing a "No Data" recording?\n-   *\n-   * @param testMe The {@link Airing} to check.\n-   * @return <code>true</code> if the airing is a "No Data" recording.\n-   */\n-  public boolean isNoShow(Airing testMe)\n-  {\n-    return testMe != null && testMe.showID == noShowID;\n-  }\n-\n   void mpause() { if (primed) { try{Thread.sleep(Sage.EMBEDDED ? 300 : 50);}catch(Exception e){} } }\n \n   void maintenance()\n@@ -5647,6 +5636,7 @@ public class Wizard implements EPGDBPublic2\n       acquireWriteLock(PEOPLE_CODE);\n       person = (oldPerson != null) ? (Person) oldPerson.clone() : new Person(getNextWizID());\n       person.name = new String(name);\n+      person.ignoreCaseHash = name.toLowerCase().hashCode();\n       person.extID = extID;\n       person.dateOfBirth = dob;\n       person.dateOfDeath = dod;\n@@ -5735,6 +5725,7 @@ public class Wizard implements EPGDBPublic2\n         }\n         Person rv = new Person(getNextWizID());\n         rv.name = new String(name);\n+        rv.ignoreCaseHash = name.toLowerCase().hashCode();\n         rv.setMediaMask(DBObject.MEDIA_MASK_TV);\n         // We use a negative external ID for a Person to indicate an alias to the real one stored in the DB\n         rv.extID = -1 * extID;\n@@ -5824,6 +5815,7 @@ public class Wizard implements EPGDBPublic2\n       {\n         Person rv = new Person(getNextWizID());\n         rv.name = new String(name);\n+        rv.ignoreCaseHash = name.toLowerCase().hashCode();\n         rv.setMediaMask(createMediaMask);\n         rv.extID = 0;\n         t.add(rv, !loading);\n@@ -8374,6 +8366,7 @@ public class Wizard implements EPGDBPublic2\n       if ( rv == null ) {\n         rv = new Stringer(getNextWizID());\n         rv.name = name;\n+        rv.ignoreCaseHash = name.toLowerCase().hashCode();\n         rv.setMediaMask(createMediaMask);\n         t.add(rv, true);\n       } else {\n@@ -8505,6 +8498,7 @@ public class Wizard implements EPGDBPublic2\n         return rv;\n       rv = new Stringer(getNextWizID());\n       rv.name = new String(name);\n+      rv.ignoreCaseHash = name.toLowerCase().hashCode();\n       rv.setMediaMask(createMediaMask);\n       t.add(rv, !loading);\n       return rv;\n@@ -8708,6 +8702,17 @@ public class Wizard implements EPGDBPublic2\n \n   boolean isStandalone() { return standalone; }\n \n+  /**\n+   * Is an airing a "No Data" recording?\n+   *\n+   * @param testMe The {@link Airing} to check.\n+   * @return <code>true</code> if the airing is a "No Data" recording.\n+   */\n+  public boolean isNoShow(Airing testMe)\n+  {\n+    return testMe != null && testMe.showID == noShowID;\n+  }\n+\n   boolean isNoShow(Show s) { return noShow == s; }\n \n   boolean isNoShow(int showID) { return noShow.id == showID; }\ndiff --git a/java/sage/api/AiringAPI.java b/java/sage/api/AiringAPI.java\nindex 4d1bf7f8..94541225 100644\n--- a/java/sage/api/AiringAPI.java\n+++ b/java/sage/api/AiringAPI.java\n@@ -16,7 +16,6 @@\n package sage.api;\n \n import sage.UIClient;\n-import sage.UIManager;\n \n import sage.*;\n \n@@ -610,7 +609,7 @@ public class AiringAPI{\n           // require them to resolve the conflict interactively. It can be dealt with in the\n           // Recording Information Menu.\n           mr.setRecordingQuality(q);\n-          Scheduler.getInstance().kick(false); // because it can change the encoders for it\n+          SchedulerSelector.getInstance().kick(false); // because it can change the encoders for it\n         }\n         return null;\n       }});\n@@ -652,10 +651,10 @@ public class AiringAPI{\n             {\n               Object rv;\n               if (mr != null)\n-                rv = Catbert.getRecordFailureObject(Seeker.getInstance().modifyRecord(start - mr.getStartTime(),\n+                rv = Catbert.getRecordFailureObject(SeekerSelector.getInstance().modifyRecord(start - mr.getStartTime(),\n                     stop - mr.getEndTime(), mr, uiClient));\n               else\n-                rv = Catbert.getRecordFailureObject(Seeker.getInstance().timedRecord(start, stop, a.getStationID(), 0, a, uiClient));\n+                rv = Catbert.getRecordFailureObject(SeekerSelector.getInstance().timedRecord(start, stop, a.getStationID(), 0, a, uiClient));\n               Catbert.asyncTaskComplete(taskID, rv);\n             }\n           }, "AsyncChannelSetRecordingTimes");\n@@ -683,7 +682,7 @@ public class AiringAPI{\n           {\n             public void run()\n             {\n-              Catbert.asyncTaskComplete(taskID, Catbert.getRecordFailureObject(Seeker.getInstance().record(a, uiClient)));\n+              Catbert.asyncTaskComplete(taskID, Catbert.getRecordFailureObject(SeekerSelector.getInstance().record(a, uiClient)));\n             }\n           }, "AsyncRecord");\n           return taskID;\n@@ -702,7 +701,7 @@ public class AiringAPI{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         Airing a = getAir(stack);\n         if (Permissions.hasPermission(Permissions.PERMISSION_RECORDINGSCHEDULE, stack.getUIMgr()))\n-          Seeker.getInstance().cancelRecord(a, Sage.client ? stack.getUIMgrSafe() : stack.getUIMgr());\n+          SeekerSelector.getInstance().cancelRecord(a, Sage.client ? stack.getUIMgrSafe() : stack.getUIMgr());\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Airing", "IsAiringObject", 1, new String[] { "Airing" })\ndiff --git a/java/sage/api/CaptureDeviceAPI.java b/java/sage/api/CaptureDeviceAPI.java\nindex d3d9b273..23c79c94 100644\n--- a/java/sage/api/CaptureDeviceAPI.java\n+++ b/java/sage/api/CaptureDeviceAPI.java\n@@ -93,7 +93,7 @@ public class CaptureDeviceAPI{\n        * @declaration public String[] GetActiveCaptureDevices();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Scheduler.getInstance().getMyEncoderNames();\n+        return SchedulerSelector.getInstance().getMyEncoderNames();\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDevice", "IsCaptureDeviceInUseByALiveClient", 1, new String[] {"CaptureDevice"}, true)\n     {\n@@ -105,7 +105,7 @@ public class CaptureDeviceAPI{\n        * @declaration public boolean IsCaptureDeviceInUseByALiveClient(String CaptureDevice);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Boolean.valueOf(Seeker.getInstance().isAClientControllingCaptureDevice(getCapDev(stack)));\n+        return Boolean.valueOf(SeekerSelector.getInstance().isAClientControllingCaptureDevice(getCapDev(stack)));\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDevice", "AddInputForRFChannel", 2, new String[] {"CaptureDevice", "RFChannel"}, true)\n     {\n@@ -164,7 +164,7 @@ public class CaptureDeviceAPI{\n        * @declaration public MediaFile GetCaptureDeviceCurrentRecordFile(String CaptureDevice);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getCurrRecordMediaFile(getCapDev(stack));\n+        return SeekerSelector.getInstance().getCurrRecordMediaFile(getCapDev(stack));\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDevice", "GetCaptureDeviceQualities", 1, new String[] {"CaptureDevice"}, true)\n     {\n@@ -311,7 +311,7 @@ public class CaptureDeviceAPI{\n           ((DShowCaptureDevice) cdev).setDTVStandard(std);\n         }\n         return null;\n-      }});\r\n+      }});\n     rft.put(new PredefinedJEPFunction("CaptureDevice", "GetCaptureDeviceMerit", new String[] {"CaptureDevice"}, true)\n     {\n       /**\n@@ -321,25 +321,25 @@ public class CaptureDeviceAPI{\n        *\n        * @declaration public int GetCaptureDeviceMerit(String CaptureDevice);\n        */\n-      public Object runSafely(Catbert.FastStack stack) throws Exception{\r\n-        CaptureDevice capDev = getCapDev(stack);\r\n+      public Object runSafely(Catbert.FastStack stack) throws Exception{\n+        CaptureDevice capDev = getCapDev(stack);\n         if (capDev != null)\n           return capDev.getMerit();\n-        else\r\n-          return 0;\r\n-      }});\r\n+        else\n+          return 0;\n+      }});\n     rft.put(new PredefinedJEPFunction("CaptureDevice", "SetCaptureDeviceMerit", new String[] {"CaptureDevice", "Merit"}, true)\n     {\n-      /**\r\n+      /**\n        * Sets the encoder merit for a CaptureDevice.\n        * @param Merit The new merit value for the specified CaptureDevice\n        *\n        * @declaration public void SetCaptureDeviceMerit(String CaptureDevice, int Merit);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        int Merit = getInt(stack);\r\n-        CaptureDevice capDev = getCapDev(stack);\r\n-        if (capDev != null)\r\n+        int Merit = getInt(stack);\n+        CaptureDevice capDev = getCapDev(stack);\n+        if (capDev != null)\n           capDev.setMerit(Merit);\n         return null;\n       }});\ndiff --git a/java/sage/api/CaptureDeviceInputAPI.java b/java/sage/api/CaptureDeviceInputAPI.java\nindex 1891f525..cb823698 100644\n--- a/java/sage/api/CaptureDeviceInputAPI.java\n+++ b/java/sage/api/CaptureDeviceInputAPI.java\n@@ -499,7 +499,7 @@ public class CaptureDeviceInputAPI{\n             ds.setEnabled(true);\n         }\n         getCapDevInput(stack).setProvider(provID);\n-        Scheduler.getInstance().kick(false);\n+        SchedulerSelector.getInstance().kick(false);\n         return Boolean.TRUE;\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDeviceInput", "ConfigureInputWithoutEPGData", 1, new String[] {"CaptureDeviceInput"}, true)\n@@ -532,7 +532,7 @@ public class CaptureDeviceInputAPI{\n         else\n           ds.setEnabled(true);\n         cdi.setProvider(provID);\n-        Scheduler.getInstance().kick(false);\n+        SchedulerSelector.getInstance().kick(false);\n         return Boolean.TRUE;\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDeviceInput", "ReleaseCaptureDeviceInput", 1, new String[] {"CaptureDeviceInput"}, true)\n@@ -546,7 +546,7 @@ public class CaptureDeviceInputAPI{\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         getCapDevInput(stack).setProvider(0);\n-        Scheduler.getInstance().kick(false);\n+        SchedulerSelector.getInstance().kick(false);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDeviceInput", "GetCaptureDeviceInputBeingViewed")\n@@ -580,7 +580,7 @@ public class CaptureDeviceInputAPI{\n        * @declaration public String GetCaptureDeviceInputRecordingMediaFile(MediaFile MediaFile);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        Object rv = Seeker.getInstance().getInputForCurrRecordingFile(getMediaFile(stack));\n+        Object rv = SeekerSelector.getInstance().getInputForCurrRecordingFile(getMediaFile(stack));\n         return (rv == null) ? null : rv.toString();\n       }});\n     rft.put(new PredefinedJEPFunction("CaptureDeviceInput", "GetLineupForCaptureDeviceInput", 1, new String[] {"CaptureDeviceInput"})\ndiff --git a/java/sage/api/Configuration.java b/java/sage/api/Configuration.java\nindex ee61eb6c..e3d126d7 100644\n--- a/java/sage/api/Configuration.java\n+++ b/java/sage/api/Configuration.java\n@@ -27,6 +27,8 @@ import sage.DirectX9SageRenderer;\n import sage.EPG;\n import sage.EventRouter;\n import sage.ExternalTuningManager;\n+import sage.SchedulerSelector;\n+import sage.SeekerSelector;\n import sage.Java2DSageRenderer;\n import sage.MMC;\n import sage.MiniClientSageRenderer;\n@@ -75,7 +77,7 @@ public class Configuration{\n        * @declaration public java.io.File[] GetLibraryImportPaths();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getArchiveDirectories(Seeker.ALL_DIR_MASK);\n+        return SeekerSelector.getInstance().getArchiveDirectories(Seeker.ALL_DIR_MASK);\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetPictureLibraryImportPaths", true)\n     {\n@@ -86,7 +88,7 @@ public class Configuration{\n        * @declaration public java.io.File[] GetPictureLibraryImportPaths();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getArchiveDirectories(Seeker.PICTURE_DIR_MASK);\n+        return SeekerSelector.getInstance().getArchiveDirectories(Seeker.PICTURE_DIR_MASK);\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetMusicLibraryImportPaths", true)\n     {\n@@ -97,7 +99,7 @@ public class Configuration{\n        * @declaration public java.io.File[] GetMusicLibraryImportPaths();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getArchiveDirectories(Seeker.MUSIC_DIR_MASK);\n+        return SeekerSelector.getInstance().getArchiveDirectories(Seeker.MUSIC_DIR_MASK);\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetVideoLibraryImportPaths", true)\n     {\n@@ -108,7 +110,7 @@ public class Configuration{\n        * @declaration public java.io.File[] GetVideoLibraryImportPaths();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getArchiveDirectories(Seeker.VIDEO_DIR_MASK);\n+        return SeekerSelector.getInstance().getArchiveDirectories(Seeker.VIDEO_DIR_MASK);\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "IsMajorMinorDTVChannelTuningEnabled", true)\n     {\n@@ -120,7 +122,7 @@ public class Configuration{\n        * @declaration public boolean IsMajorMinorDTVChannelTuningEnabled();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Boolean.valueOf(Seeker.getInstance().getUseDTVMajorMinorChans());\n+        return Boolean.valueOf(SeekerSelector.getInstance().getUseDTVMajorMinorChans());\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetAutodialForInternet", true)\n     {\n@@ -914,7 +916,7 @@ public class Configuration{\n        * @declaration public String GetDefaultRecordingQuality();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getDefaultQuality();\n+        return SeekerSelector.getInstance().getDefaultQuality();\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "IsIntelligentRecordingDisabled", true)\n     {\n@@ -925,7 +927,7 @@ public class Configuration{\n        * @declaration public boolean IsIntelligentRecordingDisabled();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Boolean.valueOf(Seeker.getInstance().getDisableProfilerRecording());\n+        return Boolean.valueOf(SeekerSelector.getInstance().getDisableProfilerRecording());\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "AddLibraryImportPath", 1, new String[]{"NewPath"}, true)\n     {\n@@ -939,7 +941,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         String f = getString(stack);\n         if (f != null)\n-          Seeker.getInstance().addArchiveDirectory(f, Seeker.ALL_DIR_MASK);\n+          SeekerSelector.getInstance().addArchiveDirectory(f, Seeker.ALL_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "AddPictureLibraryImportPath", 1, new String[]{"NewPath"}, true)\n@@ -954,7 +956,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         String f = getString(stack);\n         if (f != null)\n-          Seeker.getInstance().addArchiveDirectory(f, Seeker.PICTURE_DIR_MASK);\n+          SeekerSelector.getInstance().addArchiveDirectory(f, Seeker.PICTURE_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "AddMusicLibraryImportPath", 1, new String[]{"NewPath"}, true)\n@@ -969,7 +971,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         String f = getString(stack);\n         if (f != null)\n-          Seeker.getInstance().addArchiveDirectory(f, Seeker.MUSIC_DIR_MASK);\n+          SeekerSelector.getInstance().addArchiveDirectory(f, Seeker.MUSIC_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "AddVideoLibraryImportPath", 1, new String[]{"NewPath"}, true)\n@@ -984,7 +986,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         String f = getString(stack);\n         if (f != null)\n-          Seeker.getInstance().addArchiveDirectory(f, Seeker.VIDEO_DIR_MASK);\n+          SeekerSelector.getInstance().addArchiveDirectory(f, Seeker.VIDEO_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "RemoveLibraryImportPath", 1, new String[]{"RemovePath"}, true)\n@@ -998,7 +1000,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         java.io.File f = getFile(stack);\n         if (f != null)\n-          Seeker.getInstance().removeArchiveDirectory(f, Seeker.ALL_DIR_MASK);\n+          SeekerSelector.getInstance().removeArchiveDirectory(f, Seeker.ALL_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "RemovePictureLibraryImportPath", 1, new String[]{"RemovePath"}, true)\n@@ -1012,7 +1014,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         java.io.File f = getFile(stack);\n         if (f != null)\n-          Seeker.getInstance().removeArchiveDirectory(f, Seeker.PICTURE_DIR_MASK);\n+          SeekerSelector.getInstance().removeArchiveDirectory(f, Seeker.PICTURE_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "RemoveMusicLibraryImportPath", 1, new String[]{"RemovePath"}, true)\n@@ -1026,7 +1028,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         java.io.File f = getFile(stack);\n         if (f != null)\n-          Seeker.getInstance().removeArchiveDirectory(f, Seeker.MUSIC_DIR_MASK);\n+          SeekerSelector.getInstance().removeArchiveDirectory(f, Seeker.MUSIC_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "RemoveVideoLibraryImportPath", 1, new String[]{"RemovePath"}, true)\n@@ -1040,7 +1042,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         java.io.File f = getFile(stack);\n         if (f != null)\n-          Seeker.getInstance().removeArchiveDirectory(f, Seeker.VIDEO_DIR_MASK);\n+          SeekerSelector.getInstance().removeArchiveDirectory(f, Seeker.VIDEO_DIR_MASK);\n         return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "SetDefaultRecordingQuality", 1, new String[]{"Quality"}, true)\n@@ -1052,7 +1054,7 @@ public class Configuration{\n        * @declaration public void SetDefaultRecordingQuality(String Quality);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        Seeker.getInstance().setDefaultQuality(getString(stack)); return null;\n+        SeekerSelector.getInstance().setDefaultQuality(getString(stack)); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "SetIntelligentRecordingDisabled", 1, new String[]{"Disabled"}, true)\n     {\n@@ -1063,7 +1065,7 @@ public class Configuration{\n        * @declaration public void SetIntelligentRecordingDisabled(boolean Disabled);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        Seeker.getInstance().setDisableProfilerRecording(evalBool(stack.pop())); return null;\n+        SeekerSelector.getInstance().setDisableProfilerRecording(evalBool(stack.pop())); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "SetMajorMinorDTVChannelTuningEnabled", 1, new String[]{"Enabled"}, true)\n     {\n@@ -1075,7 +1077,7 @@ public class Configuration{\n        * @declaration public void SetMajorMinorDTVChannelTuningEnabled(boolean Enabled);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        Seeker.getInstance().setUseDTVMajorMinorChans(evalBool(stack.pop())); return null;\n+        SeekerSelector.getInstance().setUseDTVMajorMinorChans(evalBool(stack.pop())); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "IsScreenSaverOnSleepEnabled")\n     {\n@@ -1475,7 +1477,7 @@ public class Configuration{\n        * @declaration public String[] GetRecordingQualities();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        CaptureDevice[] capdevs = Scheduler.getInstance().getMyEncoders();\n+        CaptureDevice[] capdevs = SchedulerSelector.getInstance().getMyEncoders();\n         if (capdevs.length == 0)\n           return MMC.getInstance().getAllEncodingQualities();\n         else\n@@ -1523,7 +1525,7 @@ public class Configuration{\n        * @declaration public java.io.File[] GetVideoDirectories();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getVideoStoreDirectories();\n+        return SeekerSelector.getInstance().getVideoStoreDirectories();\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetDiskspaceRuleForVideoDirectory", 1, new String[] {"Directory"}, true)\n     {\n@@ -1536,7 +1538,7 @@ public class Configuration{\n        * @declaration public String GetDiskspaceRuleForVideoDirectory(java.io.File Directory);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getRuleForDirectory(getFile(stack));\n+        return SeekerSelector.getInstance().getRuleForDirectory(getFile(stack));\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetDiskspaceSizeForVideoDirectory", 1, new String[] {"Directory"}, true)\n     {\n@@ -1548,7 +1550,7 @@ public class Configuration{\n        * @declaration public long GetDiskspaceSizeForVideoDirectory(java.io.File Directory);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return new Long(Seeker.getInstance().getRuleSizeForDirectory(getFile(stack)));\n+        return new Long(SeekerSelector.getInstance().getRuleSizeForDirectory(getFile(stack)));\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "RemoveVideoDirectory", 1, new String[] {"Directory"}, true)\n     {\n@@ -1561,7 +1563,7 @@ public class Configuration{\n        * @declaration public void RemoveVideoDirectory(java.io.File Directory);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        Seeker.getInstance().removeVideoDirectory(getFile(stack)); return null;\n+        SeekerSelector.getInstance().removeVideoDirectory(getFile(stack)); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "AddVideoDirectory", 3, new String[] {"Directory", "Rule", "Size"}, true)\n     {\n@@ -1577,7 +1579,7 @@ public class Configuration{\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         long l = getLong(stack);\n         String s = getString(stack);\n-        Seeker.getInstance().addVideoDirectory(getString(stack), s, l); return null;\n+        SeekerSelector.getInstance().addVideoDirectory(getString(stack), s, l); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "ChangeVideoDirectory", 4, new String[] { "OldDirectory", "NewDirectory", "NewRule", "NewSize"}, true)\n     {\n@@ -1594,7 +1596,7 @@ public class Configuration{\n         long l = getLong(stack);\n         String s = getString(stack);\n         java.io.File newDir = getFile(stack);\n-        Seeker.getInstance().changeVideoDirectory(getFile(stack), newDir, s, l); return null;\n+        SeekerSelector.getInstance().changeVideoDirectory(getFile(stack), newDir, s, l); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetVideoDiskspaceRules")\n     {\n@@ -1605,7 +1607,7 @@ public class Configuration{\n        * @declaration public String[] GetVideoDiskspaceRules();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getVideoDiskspaceRules();\n+        return SeekerSelector.getVideoDiskspaceRules();\n       }});\n     rft.put(new PredefinedJEPFunction("Configuration", "GetIRCodesLinkedToSageCommand", 1, new String[] {"SageCommand"})\n     {\ndiff --git a/java/sage/api/Database.java b/java/sage/api/Database.java\nindex 89382b6f..505cf4fd 100644\n--- a/java/sage/api/Database.java\n+++ b/java/sage/api/Database.java\n@@ -689,7 +689,7 @@ public class Database {\n         {\n           bond = null;\n           // Search through all of the Favorite objects to find the correct one\n-          StringBuffer sbCache = new StringBuffer();\n+          StringBuilder sbCache = new StringBuilder();\n           for (int i = 0; i < cachedFavs.length; i++)\n           {\n             if (cachedFavs[i].followsTrend(air, false, sbCache))\n@@ -1635,7 +1635,7 @@ public class Database {\n             return (l1 < l2) ? -1 : ((l1 > l2) ? 1 : 0);\n           }\n         });\n-        optSortMap.put("intelligent", Seeker.getInstance().getMediaFileComparator(true));\n+        optSortMap.put("intelligent", SeekerSelector.getInstance().getMediaFileComparator(true));\n         optSortMap.put("caseinsensitive", new java.util.Comparator()\n         {\n           public int compare(Object o1, Object o2)\ndiff --git a/java/sage/api/FavoriteAPI.java b/java/sage/api/FavoriteAPI.java\nindex dfb3fab0..b1c2aa7d 100644\n--- a/java/sage/api/FavoriteAPI.java\n+++ b/java/sage/api/FavoriteAPI.java\n@@ -459,7 +459,7 @@ public class FavoriteAPI {\n         if (Permissions.hasPermission(Permissions.PERMISSION_RECORDINGSCHEDULE, stack.getUIMgr()))\n         {\n           Carny.getInstance().setRecordingQuality(a, q);\n-          Scheduler.getInstance().kick(false); // because it can change the encoders for it\n+          SchedulerSelector.getInstance().kick(false); // because it can change the encoders for it\n         }\n         return null;\n       }});\n@@ -535,7 +535,7 @@ public class FavoriteAPI {\n           if (mr != null && q != null && Permissions.hasPermission(Permissions.PERMISSION_RECORDINGSCHEDULE, stack.getUIMgr()))\n           {\n             mr.bully(q);\n-            Scheduler.getInstance().kick(false);\n+            SchedulerSelector.getInstance().kick(false);\n           }\n         }\n         return null;\n@@ -1003,7 +1003,7 @@ public class FavoriteAPI {\n         {\n           // Search through all of the Favorite objects to find the correct one\n           Agent[] favs = Wizard.getInstance().getFavorites();\n-          StringBuffer sbCache = new StringBuffer();\n+          StringBuilder sbCache = new StringBuilder();\n           for (int i = 0; i < favs.length; i++)\n           {\n             if (favs[i].followsTrend(a, false, sbCache))\n@@ -1027,7 +1027,7 @@ public class FavoriteAPI {\n         java.util.ArrayList rv = new java.util.ArrayList();\n         // Search through all of the Favorite objects to find the correct one\n         Agent[] favs = Wizard.getInstance().getFavorites();\n-        StringBuffer sbCache = new StringBuffer();\n+        StringBuilder sbCache = new StringBuilder();\n         for (int i = 0; i < favs.length; i++)\n         {\n           if (favs[i].followsTrend(a, false, sbCache))\n@@ -1177,7 +1177,7 @@ public class FavoriteAPI {\n         Agent fav = (Agent) stack.pop();\n         if (fav == null) return null;\n         return fav.getRelatedAirings(Wizard.getInstance().getRawAccess(Wizard.AIRING_CODE,\n-            Wizard.AIRINGS_BY_CT_CODE), true, false, new StringBuffer());\n+            Wizard.AIRINGS_BY_CT_CODE), false, false, new StringBuilder());\n       }});\n     rft.put(new PredefinedJEPFunction("Favorite", "GetPotentialFavoriteAirings", new String[] { "Favorite" }, true)\n     {\n@@ -1195,7 +1195,7 @@ public class FavoriteAPI {\n         Agent fav = (Agent) stack.pop();\n         if (fav == null) return null;\n         return fav.getRelatedAirings(Wizard.getInstance().getRawAccess(Wizard.AIRING_CODE,\n-            Wizard.AIRINGS_BY_CT_CODE), true, false, true, new StringBuffer());\n+            Wizard.AIRINGS_BY_CT_CODE), false, true, new StringBuilder());\n       }});\n     rft.put(new PredefinedJEPFunction("Favorite", "GetFavoriteID", new String[] { "Favorite" })\n     {\ndiff --git a/java/sage/api/Global.java b/java/sage/api/Global.java\nindex cac7093a..881e4fbb 100644\n--- a/java/sage/api/Global.java\n+++ b/java/sage/api/Global.java\n@@ -25,6 +25,8 @@ import sage.EPGDataSource;\n import sage.ExternalTuningManager;\n import sage.FileDownloader;\n import sage.FileTransfer;\n+import sage.SchedulerSelector;\n+import sage.SeekerSelector;\n import sage.IOUtils;\n import sage.LinuxUtils;\n import sage.MMC;\n@@ -195,7 +197,7 @@ public class Global {\n        * @declaration public boolean AreThereUnresolvedConflicts();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Boolean.valueOf(Scheduler.getInstance().areThereDontKnows());\n+        return Boolean.valueOf(SchedulerSelector.getInstance().areThereDontKnows());\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "IsAsleep")\n     {\n@@ -217,7 +219,7 @@ public class Global {\n        * @declaration public long GetTotalDiskspaceAvailable();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return new Long(Seeker.getInstance().getAvailVideoDiskspace());\n+        return new Long(SeekerSelector.getInstance().getAvailVideoDiskspace());\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetTotalLibraryDuration", true)\n     {\n@@ -228,7 +230,7 @@ public class Global {\n        * @declaration public long GetTotalLibraryDuration();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return new Long(Seeker.getInstance().getTotalImportedLibraryDuration());\n+        return new Long(SeekerSelector.getInstance().getTotalImportedLibraryDuration());\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetTotalVideoDuration", true)\n     {\n@@ -239,7 +241,7 @@ public class Global {\n        * @declaration public long GetTotalVideoDuration();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return new Long(Seeker.getInstance().getTotalVideoDuration());\n+        return new Long(SeekerSelector.getInstance().getTotalVideoDuration());\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetUsedLibraryDiskspace", true)\n     {\n@@ -250,7 +252,7 @@ public class Global {\n        * @declaration public long GetUsedLibraryDiskspace();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return new Long(Seeker.getInstance().getUsedImportedLibraryDiskspace());\n+        return new Long(SeekerSelector.getInstance().getUsedImportedLibraryDiskspace());\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetUsedVideoDiskspace", true)\n     {\n@@ -261,7 +263,7 @@ public class Global {\n        * @declaration public long GetUsedVideoDiskspace();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return new Long(Seeker.getInstance().getUsedVideoDiskspace());\n+        return new Long(SeekerSelector.getInstance().getUsedVideoDiskspace());\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "AreAiringsSameShow", 2, new String[] {"Airing1", "Airing2"})\n     {\n@@ -429,7 +431,7 @@ public class Global {\n        * @declaration public MediaFile[] GetCurrentlyRecordingMediaFiles();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getCurrRecordFiles();\n+        return SeekerSelector.getInstance().getCurrRecordFiles();\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetSuggestedIntelligentRecordings", true)\n     {\n@@ -442,7 +444,7 @@ public class Global {\n        * @declaration public Airing[] GetSuggestedIntelligentRecordings();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getIRScheduledAirings();\n+        return SeekerSelector.getInstance().getIRScheduledAirings();\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetScheduledRecordings", true)\n     {\n@@ -453,7 +455,7 @@ public class Global {\n        * @declaration public Airing[] GetScheduledRecordings();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getInterleavedScheduledAirings();\n+        return SeekerSelector.getInstance().getInterleavedScheduledAirings();\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetScheduledRecordingsForDevice", 1, new String[] { "CaptureDevice"}, true)\n     {\n@@ -465,7 +467,7 @@ public class Global {\n        * @declaration public Airing[] GetScheduledRecordingsForDevice(String CaptureDevice);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getScheduledAiringsForSource(getString(stack));\n+        return SeekerSelector.getInstance().getScheduledAiringsForSource(getString(stack));\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetScheduledRecordingsForTime", 2, new String[] { "StartTime", "StopTime" }, true)\n     {\n@@ -479,7 +481,7 @@ public class Global {\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         long stop = getLong(stack);\n-        return Seeker.getInstance().getInterleavedScheduledAirings(getLong(stack), stop);\n+        return SeekerSelector.getInstance().getInterleavedScheduledAirings(getLong(stack), stop);\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetScheduledRecordingsForDeviceForTime", 3, new String[] { "CaptureDevice", "StartTime", "StopTime"}, true)\n     {\n@@ -495,7 +497,7 @@ public class Global {\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         long stop = getLong(stack);\n         long start = getLong(stack);\n-        return Seeker.getInstance().getScheduledAiringsForSource(getString(stack), start, stop);\n+        return SeekerSelector.getInstance().getScheduledAiringsForSource(getString(stack), start, stop);\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "GetRecentlyWatched", 1, new String[] {"DurationToLookBack"}, true)\n     {\n@@ -507,7 +509,7 @@ public class Global {\n        * @declaration public Airing[] GetRecentlyWatched(long DurationToLookBack);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().getRecentWatches(Sage.time() - getLong(stack));\n+        return SeekerSelector.getInstance().getRecentWatches(Sage.time() - getLong(stack));\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "RunLibraryImportScan", 1, new String[] {"WaitUntilDone"}, true)\n     {\n@@ -518,7 +520,7 @@ public class Global {\n        * @declaration public void RunLibraryImportScan(boolean WaitUntilDone);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        Seeker.getInstance().scanLibrary(Catbert.evalBool(stack.pop())); return null;\n+        SeekerSelector.getInstance().scanLibrary(Catbert.evalBool(stack.pop())); return null;\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "Exit")\n     {\n@@ -587,7 +589,7 @@ public class Global {\n           }\n         }\n         EPG.getInstance().kick();\n-        Scheduler.getInstance().kick(false);\n+        SchedulerSelector.getInstance().kick(false);\n         if (Sage.client)\n         {\n           return makeNetworkedCall(stack);\n@@ -1653,8 +1655,8 @@ public class Global {\n        * @declaration public java.util.Vector GetAiringsThatWontBeRecorded(boolean OnlyUnresolved);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        java.util.Map conMap = Catbert.evalBool(stack.pop()) ? Scheduler.getInstance().getUnresolvedConflictsMap() :\n-          Scheduler.getInstance().getConflictsMap();\n+        java.util.Map conMap = Catbert.evalBool(stack.pop()) ? SchedulerSelector.getInstance().getUnresolvedConflictsMap() :\n+          SchedulerSelector.getInstance().getConflictsMap();\n         java.util.Vector rv = new java.util.Vector();\n         java.util.Iterator walker = conMap.values().iterator();\n         while (walker.hasNext())\n@@ -1907,7 +1909,7 @@ public class Global {\n \t\t\t\t\t\t\t}\n \t\t\t\t\t\t}\n \t\t\t\t\t}*/\n-            rv = Catbert.getRecordFailureObject(Seeker.getInstance().timedRecord(start, stop, c.getStationID(), recurCode, baseAir, uiClient));\n+            rv = Catbert.getRecordFailureObject(SeekerSelector.getInstance().timedRecord(start, stop, c.getStationID(), recurCode, baseAir, uiClient));\n             Catbert.asyncTaskComplete(taskID, rv);\n           }\n         }, "AsyncCreateTimedRecording");\n@@ -2946,7 +2948,7 @@ public class Global {\n        * @declaration public boolean IsDoingLibraryImportScan();\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Seeker.getInstance().isDoingImportScan() ? Boolean.TRUE : Boolean.FALSE;\n+        return SeekerSelector.getInstance().isDoingImportScan() ? Boolean.TRUE : Boolean.FALSE;\n       }});\n     rft.put(new PredefinedJEPFunction("Global", "SwitchEmbeddedModeTo", new String[] { "NewMode" })\n     {\n@@ -3013,7 +3015,7 @@ public class Global {\n         if (Sage.DBG) System.out.println("Preparing the system for a firmware load...free all the memory we can...");\n         if (Sage.DBG) System.out.println("MemStats-1: Used=" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())/1000000 +\n             "MB Total=" + Runtime.getRuntime().totalMemory()/1000000 + "MB Max=" + Runtime.getRuntime().maxMemory()/1000000 + "MB");\n-        Seeker.getInstance().disableLibraryScanning();\n+        SeekerSelector.getInstance().disableLibraryScanning();\n         // Kill the process for recording as well\n         IOUtils.exec2(new String[] { "sh", "-c", "kill `pidof SGMRecording`"}, true);\n         if (stack.getUIMgrSafe() != null)\ndiff --git a/java/sage/api/MediaFileAPI.java b/java/sage/api/MediaFileAPI.java\nindex 3c573ac2..dc54d92c 100644\n--- a/java/sage/api/MediaFileAPI.java\n+++ b/java/sage/api/MediaFileAPI.java\n@@ -565,7 +565,7 @@ public class MediaFileAPI {\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n         MediaFile mf = getMediaFile(stack); if (mf == null || !Permissions.hasPermission(Permissions.PERMISSION_DELETE, stack.getUIMgr())) return Boolean.FALSE;\n-        return Boolean.valueOf(Seeker.getInstance().destroyFile(mf, true, "User", stack.getUIMgr() != null ? stack.getUIMgr().getLocalUIClientName() : null));\n+        return Boolean.valueOf(SeekerSelector.getInstance().destroyFile(mf, true, "User", stack.getUIMgr() != null ? stack.getUIMgr().getLocalUIClientName() : null));\n       }});\n     rft.put(new PredefinedJEPFunction("MediaFile", "DeleteFileWithoutPrejudice", 1, new String[] { "MediaFile" }, true)\n     {\n@@ -583,7 +583,7 @@ public class MediaFileAPI {\n         MediaFile mf = getMediaFile(stack); if (mf == null || !Permissions.hasPermission(Permissions.PERMISSION_DELETE, stack.getUIMgr())) return Boolean.FALSE;\n         if (Permissions.hasPermission(Permissions.PERMISSION_RECORDINGSCHEDULE, stack.getUIMgr()))\n           BigBrother.clearWatched(mf.getContentAiring());\n-        return Boolean.valueOf(Seeker.getInstance().destroyFile(mf, false, "User", stack.getUIMgr() != null ? stack.getUIMgr().getLocalUIClientName() : null));\n+        return Boolean.valueOf(SeekerSelector.getInstance().destroyFile(mf, false, "User", stack.getUIMgr() != null ? stack.getUIMgr().getLocalUIClientName() : null));\n       }});\n     rft.put(new PredefinedJEPFunction("MediaFile", "GetFileDuration", 1, new String[] { "MediaFile" })\n     {\ndiff --git a/java/sage/api/Utility.java b/java/sage/api/Utility.java\nindex c1147e27..71c85536 100644\n--- a/java/sage/api/Utility.java\n+++ b/java/sage/api/Utility.java\n@@ -33,7 +33,7 @@ import sage.Sage;\n import sage.SageConstants;\n import sage.SageRenderer;\n import sage.SageTV;\n-import sage.Seeker;\n+import sage.SeekerSelector;\n import sage.StringMatchUtils;\n import sage.UIClient;\n import sage.UIManager;\n@@ -1818,7 +1818,7 @@ public class Utility {\n           {\n             public boolean accept(java.io.File path)\n             {\n-              return path.isDirectory() || ((mediaMask & Seeker.getInstance().guessImportedMediaMaskFast(path.getAbsolutePath())) != 0);\n+              return path.isDirectory() || ((mediaMask & SeekerSelector.getInstance().guessImportedMediaMaskFast(path.getAbsolutePath())) != 0);\n             }\n           });\n         }\n@@ -2383,7 +2383,7 @@ public class Utility {\n        * @declaration public boolean IsImportableFileType(String Filename);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        return Boolean.valueOf(Seeker.getInstance().hasImportableFileExtension(getString(stack)));\n+        return Boolean.valueOf(SeekerSelector.getInstance().hasImportableFileExtension(getString(stack)));\n       }});\n     rft.put(new PredefinedJEPFunction("Utility", "GetSubnetMask")\n     {\n@@ -2490,7 +2490,7 @@ public class Utility {\n        * @declaration public String GuessMajorFileType(String Filename);\n        */\n       public Object runSafely(Catbert.FastStack stack) throws Exception{\n-        switch (Seeker.getInstance().guessImportedMediaMaskFast(getString(stack)))\n+        switch (SeekerSelector.getInstance().guessImportedMediaMaskFast(getString(stack)))\n         {\n           case DBObject.MEDIA_MASK_DVD:\n             return "D";\ndiff --git a/java/sage/epg/sd/SDRecommended.java b/java/sage/epg/sd/SDRecommended.java\nindex 8c1ba92c..e5b5c3d7 100644\n--- a/java/sage/epg/sd/SDRecommended.java\n+++ b/java/sage/epg/sd/SDRecommended.java\n@@ -21,7 +21,7 @@ import sage.DBObject;\n import sage.MediaFile;\n import sage.Pooler;\n import sage.Sage;\n-import sage.Seeker;\n+import sage.SeekerSelector;\n import sage.Show;\n import sage.TVEditorial;\n import sage.Wizard;\n@@ -152,7 +152,7 @@ public class SDRecommended\n   {\n     int alreadyExists = 0;\n     List<SDEditorial> returnValues = new ArrayList<>();\n-    Agent favorites[] = Wizard.getInstance().getFavorites();\n+    Agent agents[] = Wizard.getInstance().getFavorites();\n     TVEditorial currentEditorials[] = Wizard.getInstance().getEditorials();\n     for (SDEditorial editorial : editorials)\n     {\n@@ -161,7 +161,12 @@ public class SDRecommended\n       if (title == null || title.length() == 0)\n         continue;\n       String programId = recommendation.getProgramId();\n-      if (programId == null || programId.length() == 0)\n+      // 05/05/2016 JS: I made this a check for an expected length and specifically prepending with\n+      // a known prefix because we were throwing some garbage at SD that we shouldn\'t.\n+      if (programId == null || programId.length() == 0 ||\n+        (programId.length() != 12 && programId.length() != 14) ||\n+        (!programId.startsWith("EP") && !programId.startsWith("SH") &&\n+          !programId.startsWith("MV") && !programId.startsWith("SP")))\n         continue;\n \n       // Don\'t recommend movies that we have already seen or don\'t exist in the Wizard.\n@@ -171,8 +176,7 @@ public class SDRecommended\n         if ((show = Wizard.getInstance().getShowForExternalID(programId)) == null || show.isWatched())\n         {\n           String newId = SDUtils.fromProgramToSageTV(programId);\n-          if (newId.length() == 12 &&\n-            ((show = Wizard.getInstance().getShowForExternalID(programId)) == null || show.isWatched()))\n+          if (newId.length() == 12 && (show == null || show.isWatched()))\n             continue;\n         }\n       }\n@@ -181,9 +185,13 @@ public class SDRecommended\n       // titles or would have been recorded due to other rules, but we aren\'t too concerned about\n       // that.\n       boolean skip = false;\n-      for (Agent favorite : favorites)\n+      String lowerTitle = title.toLowerCase();\n+      for (Agent agent : agents)\n       {\n-        if (favorite.getTitle().equals(title))\n+        if (agent.testAgentFlag(Agent.DISABLED_FLAG))\n+          continue;\n+\n+        if (agent.isSameTitle(lowerTitle))\n         {\n           skip = true;\n           break;\n@@ -196,7 +204,7 @@ public class SDRecommended\n       // different external ID.\n       for (TVEditorial currentEditorial : currentEditorials)\n       {\n-        if (currentEditorial.getTitle().equals(title))\n+        if (currentEditorial.getTitle().equalsIgnoreCase(title))\n         {\n           alreadyExists++;\n           skip = true;\n@@ -283,11 +291,11 @@ public class SDRecommended\n   public static List<SDEditorial> getRecommendations(boolean includeIR) throws IOException, SDException\n   {\n     Set<String> lookupShows = new HashSet<>();\n-    Airing airings[] = Seeker.getInstance().getInterleavedScheduledAirings();\n+    Airing airings[] = SeekerSelector.getInstance().getInterleavedScheduledAirings();\n     if (Sage.DBG) System.out.println("SDEPG Using " + airings.length + " airings for recommendations");\n     if (includeIR)\n     {\n-      Airing irAirings[] = Seeker.getInstance().getIRScheduledAirings();\n+      Airing irAirings[] = SeekerSelector.getInstance().getIRScheduledAirings();\n       int copyIndex = airings.length;\n       // Don\'t use more than 3000 ever. It will cause these suggestions to drive the SD\n       // recommendations very hard towards shows that might be too derivative from what the user\n@@ -375,7 +383,30 @@ public class SDRecommended\n \n       if (Sage.DBG) System.out.println("SDEPG Using " + lookupShows.size() + " watched shows for recommendations");\n \n-      SDProgram programs[] = SDRipper.ensureSession().getPrograms(lookupShows);\n+      SDProgram programs[];\n+      try\n+      {\n+        programs = SDRipper.ensureSession().getPrograms(lookupShows);\n+      }\n+      catch (Exception e)\n+      {\n+        if (Sage.DBG) System.out.println("SDEPG Unable to perform bulk lookup: " + e.toString());\n+        ArrayList<SDProgram> newPrograms = new ArrayList<>(lookupShows.size());\n+        // We have a problem with at least one of the shows we tried to look up this is kind of\n+        // expected since we are asking for data that we were never told existed. We\'ll process them\n+        // one by one since we don\'t have any way of knowing what one it was.\n+        for (String lookupShow : lookupShows)\n+        {\n+          try\n+          {\n+            programs = SDRipper.ensureSession().getPrograms(Collections.singleton(lookupShow));\n+            if (programs[0].getCode() == 0)\n+              newPrograms.add(programs[0]);\n+            // We expect that at least one of these will fail.\n+          } catch (Exception e1) {}\n+        }\n+        programs = newPrograms.toArray(new SDProgram[newPrograms.size()]);\n+      }\n       lookupShows.clear();\n       int recommendationCount = 0;\n       for (SDProgram program : programs)\n@@ -422,7 +453,31 @@ public class SDRecommended\n       orderedPrograms.add(editorial.getRecommendation().getProgramId());\n     }\n \n-    SDProgram programs[] = SDRipper.ensureSession().getPrograms(orderedPrograms);\n+    SDProgram programs[];\n+    try\n+    {\n+      programs = SDRipper.ensureSession().getPrograms(lookupShows);\n+    }\n+    catch (Exception e)\n+    {\n+      if (Sage.DBG) System.out.println("SDEPG Unable to perform bulk lookup: " + e.toString());\n+      ArrayList<SDProgram> newPrograms = new ArrayList<>(lookupShows.size());\n+      // We have a problem with at least one of the shows we tried to look up this is kind of\n+      // expected since we are asking for data that we were never told existed. We\'ll process them\n+      // one by one since we don\'t have any way of knowing what one it was.\n+      for (String lookupShow : orderedPrograms)\n+      {\n+        try\n+        {\n+          programs = SDRipper.ensureSession().getPrograms(Collections.singleton(lookupShow));\n+          if (programs[0].getCode() == 0)\n+            newPrograms.add(programs[0]);\n+          // We expect that at least one of these will fail.\n+        } catch (Exception e1) {}\n+      }\n+      programs = newPrograms.toArray(new SDProgram[newPrograms.size()]);\n+    }\n+    lookupShows.clear();\n     for (int i = 0; i < programs.length; i++)\n     {\n       SDProgram program = programs[i];\ndiff --git a/java/sage/epg/sd/SDSession.java b/java/sage/epg/sd/SDSession.java\nindex 0dcd6d33..f9e9b262 100644\n--- a/java/sage/epg/sd/SDSession.java\n+++ b/java/sage/epg/sd/SDSession.java\n@@ -852,7 +852,7 @@ public abstract class SDSession\n     JsonArray submit = new JsonArray();\n     for (String program : programs)\n     {\n-      submit.add(program);\n+      submit.add(SDUtils.fromSageTVtoProgram(program));\n     }\n \n     SDProgram[] returnValues = postAuthJson(GET_PROGRAMS, SDProgram[].class, submit);\ndiff --git a/java/sage/locator/LocatorRegistrationClient.java b/java/sage/locator/LocatorRegistrationClient.java\nindex 83f2e100..34349f73 100644\n--- a/java/sage/locator/LocatorRegistrationClient.java\n+++ b/java/sage/locator/LocatorRegistrationClient.java\n@@ -1013,11 +1013,11 @@ public class LocatorRegistrationClient implements Runnable, LocatorConstants\n     // We need to get the folder based on file type\n     java.io.File[] libFolders = null;\n     if (theMedia.mediaType == PICTURE_TYPE)\n-      libFolders = sage.Seeker.getInstance().getArchiveDirectories(sage.Seeker.PICTURE_DIR_MASK);\n+      libFolders = sage.SeekerSelector.getInstance().getArchiveDirectories(sage.Seeker.PICTURE_DIR_MASK);\n     else if (theMedia.mediaType == AUDIO_TYPE)\n-      libFolders = sage.Seeker.getInstance().getArchiveDirectories(sage.Seeker.MUSIC_DIR_MASK);\n+      libFolders = sage.SeekerSelector.getInstance().getArchiveDirectories(sage.Seeker.MUSIC_DIR_MASK);\n     else\n-      libFolders = sage.Seeker.getInstance().getArchiveDirectories(sage.Seeker.VIDEO_DIR_MASK);\n+      libFolders = sage.SeekerSelector.getInstance().getArchiveDirectories(sage.Seeker.VIDEO_DIR_MASK);\n \n     return new java.io.File(libFolders[0], sage.Sage.rez("From_User", new Object[] { fromUser }) + java.io.File.separatorChar + theMedia.relativePath);\n   }\ndiff --git a/java/sage/msg/MsgManager.java b/java/sage/msg/MsgManager.java\nindex 05e16a47..8a39a458 100644\n--- a/java/sage/msg/MsgManager.java\n+++ b/java/sage/msg/MsgManager.java\n@@ -324,7 +324,7 @@ public class MsgManager implements Runnable\n       //if (Sage.DBG) System.out.println("ERROR couldn\'t find capture source to match epg data:" + srcName);\n       return;\n     }\n-    sage.MediaFile mf = sage.Seeker.getInstance().getCurrRecordMediaFile(capDev);\n+    sage.MediaFile mf = sage.SeekerSelector.getInstance().getCurrRecordMediaFile(capDev);\n     if (mf != null)\n     {\n       sage.media.format.ContainerFormat cf;\n@@ -380,7 +380,7 @@ public class MsgManager implements Runnable\n       return;\n     }\n     if (Sage.DBG) System.out.println("Sending the core a capture device reset request for:" + capDev);\n-    sage.Seeker.getInstance().requestDeviceReset(capDev);\n+    sage.SeekerSelector.getInstance().requestDeviceReset(capDev);\n   }\n \n   private void processSysMsg(SageMsg msg)\ndiff --git a/java/sage/util/MutableFloat.java b/java/sage/util/MutableFloat.java\nnew file mode 100644\nindex 00000000..1ee8377c\n--- /dev/null\n+++ b/java/sage/util/MutableFloat.java\n@@ -0,0 +1,72 @@\n+/*\n+ * Copyright 2015 The SageTV Authors. All Rights Reserved.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *  http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an "AS IS" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+package sage.util;\n+\n+/**\n+ * Do NOT use this class as a key on anything ever.\n+ */\n+public class MutableFloat\n+{\n+  private float value;\n+\n+  public MutableFloat()\n+  {\n+    this.value = 0.0f;\n+  }\n+\n+  public MutableFloat(float value)\n+  {\n+    this.value = value;\n+  }\n+\n+  public float getValue()\n+  {\n+    return value;\n+  }\n+\n+  public void setValue(float value)\n+  {\n+    this.value = value;\n+  }\n+\n+  public void increment(float increment)\n+  {\n+    this.value += increment;\n+  }\n+\n+  @Override\n+  public boolean equals(Object o)\n+  {\n+    if (this == o) return true;\n+    if (!(o instanceof MutableFloat)) return false;\n+\n+    MutableFloat that = (MutableFloat) o;\n+\n+    return Float.compare(that.value, value) == 0;\n+  }\n+\n+  @Override\n+  public int hashCode()\n+  {\n+    return (value != +0.0f ? Float.floatToIntBits(value) : 0);\n+  }\n+\n+  @Override\n+  public String toString()\n+  {\n+    return Float.toString(value);\n+  }\n+}\ndiff --git a/java/sage/util/MutableInteger.java b/java/sage/util/MutableInteger.java\nnew file mode 100644\nindex 00000000..d9886e5f\n--- /dev/null\n+++ b/java/sage/util/MutableInteger.java\n@@ -0,0 +1,72 @@\n+/*\n+ * Copyright 2015 The SageTV Authors. All Rights Reserved.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *  http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an "AS IS" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+package sage.util;\n+\n+/**\n+ * Do NOT use this class as a key on anything ever.\n+ */\n+public class MutableInteger\n+{\n+  private int value;\n+\n+  public MutableInteger()\n+  {\n+    this.value = 0;\n+  }\n+\n+  public MutableInteger(int value)\n+  {\n+    this.value = value;\n+  }\n+\n+  public int getValue()\n+  {\n+    return value;\n+  }\n+\n+  public void setValue(int value)\n+  {\n+    this.value = value;\n+  }\n+\n+  public void increment(int increment)\n+  {\n+    this.value += increment;\n+  }\n+\n+  @Override\n+  public boolean equals(Object o)\n+  {\n+    if (this == o) return true;\n+    if (!(o instanceof MutableInteger)) return false;\n+\n+    MutableInteger that = (MutableInteger) o;\n+\n+    return !(value != that.value);\n+  }\n+\n+  @Override\n+  public int hashCode()\n+  {\n+    return value;\n+  }\n+\n+  @Override\n+  public String toString()\n+  {\n+    return Integer.toString(value);\n+  }\n+}\ndiff --git a/java/sage/vfs/FilesystemMediaNode.java b/java/sage/vfs/FilesystemMediaNode.java\nindex 1133fc7f..1893670f 100644\n--- a/java/sage/vfs/FilesystemMediaNode.java\n+++ b/java/sage/vfs/FilesystemMediaNode.java\n@@ -103,7 +103,7 @@ public class FilesystemMediaNode extends BasicMediaNode\n         }\n         return mf.getThumbnail(loader);\n       }\n-      int mediaMask = Seeker.getInstance().guessImportedMediaMaskFast(myFile.getAbsolutePath());\n+      int mediaMask = SeekerSelector.getInstance().guessImportedMediaMaskFast(myFile.getAbsolutePath());\n       return MediaFile.getGenericImage(mediaMask, loader);\n     }\n   }\ndiff --git a/java/sage/vfs/filter/MediaMaskFilter.java b/java/sage/vfs/filter/MediaMaskFilter.java\nindex 9427fd7e..7f989b12 100644\n--- a/java/sage/vfs/filter/MediaMaskFilter.java\n+++ b/java/sage/vfs/filter/MediaMaskFilter.java\n@@ -52,7 +52,7 @@ public class MediaMaskFilter extends BasicDataObjectFilter\n     if (MediaNode.DATATYPE_FILE.equals(dataType))\n     {\n       java.io.File f = (java.io.File) data;\n-      return f.isDirectory() || (((sage.Seeker.getInstance().guessImportedMediaMaskFast(f.getAbsolutePath()) & mediaMask) == 0) == !matchPasses);\n+      return f.isDirectory() || (((sage.SeekerSelector.getInstance().guessImportedMediaMaskFast(f.getAbsolutePath()) & mediaMask) == 0) == !matchPasses);\n     }\n     else if (MediaNode.DATATYPE_MEDIAFILE.equals(dataType))\n     {\ndiff --git a/java/sage/vfs/sort/IntelligentSorter.java b/java/sage/vfs/sort/IntelligentSorter.java\nindex 3e3be225..369a7a4d 100644\n--- a/java/sage/vfs/sort/IntelligentSorter.java\n+++ b/java/sage/vfs/sort/IntelligentSorter.java\n@@ -27,7 +27,7 @@ public class IntelligentSorter extends BasicMediaNodeSorter\n   public IntelligentSorter(boolean ascending)\n   {\n     super(ascending);\n-    mycomp = sage.Seeker.getInstance().getMediaFileComparator(true);\n+    mycomp = sage.SeekerSelector.getInstance().getMediaFileComparator(true);\n   }\n \n   public int compare(Object o1, Object o2)\n', 'commit_subject': 'Merge pull request #281 from enternoescape/master', 'repo_name': 'google/sagetv', 'committer': {'name': 'GitHub', 'email': '1505422b2465e9a84f6fdfaa161078890c593f06@github.com', 'time_sec': '1495421012', 'tz_offset': '-420', 'date': {'seconds': '1495421012'}}, 'commit_tree': '8d4129b98e4ff46574c3e7e9de87a8c7554dd376', 'commit_message': 'Merge pull request #281 from enternoescape/master\n\nCarny multi-threading and more efficient processing.', 'language_name': 'C', 'license': 'apache-2.0', 'hunks': '@@ -27,7 +27,7 @@ public class IntelligentSorter extends BasicMediaNodeSorter\n   public IntelligentSorter(boolean ascending)\n   {\n     super(ascending);\n-    mycomp = sage.Seeker.getInstance().getMediaFileComparator(true);\n+    mycomp = sage.SeekerSelector.getInstance().getMediaFileComparator(true);\n   }\n \n   public int compare(Object o1, Object o2)\n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/java/sage/vfs/sort/IntelligentSorter.java', 'tgt_file': 'b/java/sage/vfs/sort/IntelligentSorter.java', 'file_extension': '.java', 'before_file': '/*\n * Copyright 2015 The SageTV Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage sage.vfs.sort;\nimport sage.vfs.*;\n\n/**\n *\n * @author Narflex\n */\npublic class IntelligentSorter extends BasicMediaNodeSorter\n{\n\n  /** Creates a new instance of IntelligentSorter */\n  public IntelligentSorter(boolean ascending)\n  {\n    super(ascending);\n    mycomp = sage.Seeker.getInstance().getMediaFileComparator(true);\n  }\n\n  public int compare(Object o1, Object o2)\n  {\n    MediaNode m1 = (MediaNode) o1;\n    MediaNode m2 = (MediaNode) o2;\n    boolean f1 = (m1 == null) ? false : m1.isFolder();\n    boolean f2 = (m2 == null) ? false : m2.isFolder();\n    if (f1 != f2)\n      return f1 ? -1 : 1;\n    // Resolve a group into its first child for sorting purposes here\n    if (f1 && m1 instanceof GrouperMediaFileNode)\n      o1 = ((GrouperMediaFileNode) m1).getFirstDescendant();\n    if (f2 && m2 instanceof GrouperMediaFileNode)\n      o2 = ((GrouperMediaFileNode) m2).getFirstDescendant();\n    o1 = m1.getDataObject();\n    o2 = m2.getDataObject();\n    if (!(o1 instanceof sage.MediaFile) && !(o1 instanceof sage.Airing))\n      o1 = sage.PredefinedJEPFunction.getMediaFileObj(o1);\n    if (!(o2 instanceof sage.MediaFile) && !(o2 instanceof sage.Airing))\n      o2 = sage.PredefinedJEPFunction.getMediaFileObj(o2);\n    return (ascending ? 1 : -1) * mycomp.compare(o1, o2);\n  }\n\n  public String getTechnique()\n  {\n    return MediaNode.SORT_BY_INTELLIGENT;\n  }\n\n  public String getName()\n  {\n    return sage.Sage.rez("Intelligent");\n  }\n\n  private java.util.Comparator mycomp;\n}\n', 'message': 'Merge pull request #281 from enternoescape/master\n\nCarny multi-threading and more efficient processing.', 'author_name': 'GitHub', 'lang': 'java'}