public static String renameForSorting(String filename) {
    if (PMS.getConfiguration().isPrettifyFilenames()) {
      // This chunk makes anime sort properly
      int squareBracketIndex;
      if (filename.substring(0, 1).matches("\\[")) {
        filename = filename.replaceAll("_", " ");
        squareBracketIndex = filename.indexOf(']');
        if (squareBracketIndex != -1) {
          filename = filename.substring(squareBracketIndex + 1);
          if (filename.substring(0, 1).matches("\\s")) {
            filename = filename.substring(1);
          }
        }
      }

      // Replace periods with spaces
      filename = filename.replaceAll("\\.", " ");
    }

    if (PMS.getConfiguration().isIgnoreTheWordThe()) {
      // Remove "The" from the beginning of files
      filename = filename.replaceAll("^(?i)The[ .]", "");
    }

    return filename;
  }
  public static void addURLResolver(URLResolver res) {
    if (urlResolvers.contains(res)) {
      return;
    }
    if (urlResolvers.isEmpty()) {
      urlResolvers.add(res);
      return;
    }

    String[] tmp = PMS.getConfiguration().getURLResolveOrder();
    if (tmp.length == 0) {
      // no order at all, just add it
      urlResolvers.add(res);
      return;
    }
    int id = -1;
    for (int i = 0; i < tmp.length; i++) {
      if (tmp[i].equalsIgnoreCase(res.name())) {
        id = i;
        break;
      }
    }

    if (id == -1) {
      // no order here, just add it
      urlResolvers.add(res);
      return;
    }
    if (id > urlResolvers.size()) {
      // add it last
      urlResolvers.add(res);
      return;
    }
    urlResolvers.add(id, res);
  }
  @Override
  public JComponent config() {
    FormLayout layout =
        new FormLayout("left:pref, 3dlu, p, 3dlu, 0:grow", "p, 3dlu, p, 3dlu, 0:grow");
    PanelBuilder builder = new PanelBuilder(layout);
    builder.setBorder(Borders.EMPTY_BORDER);
    builder.setOpaque(false);

    CellConstraints cc = new CellConstraints();

    JComponent cmp = builder.addSeparator(Messages.getString("NetworkTab.5"), cc.xyw(1, 1, 5));
    cmp = (JComponent) cmp.getComponent(0);
    cmp.setFont(cmp.getFont().deriveFont(Font.BOLD));

    builder.addLabel(Messages.getString("FFMpegDVRMSRemux.0"), cc.xy(1, 3));
    altffpath = new JTextField(PMS.getConfiguration().getFfmpegAlternativePath());
    altffpath.addKeyListener(
        new KeyListener() {
          @Override
          public void keyPressed(KeyEvent e) {}

          @Override
          public void keyTyped(KeyEvent e) {}

          @Override
          public void keyReleased(KeyEvent e) {
            PMS.getConfiguration().setFfmpegAlternativePath(altffpath.getText());
          }
        });
    builder.add(altffpath, cc.xyw(3, 3, 3));

    return builder.getPanel();
  }
  /**
   * This method populates the supplied {@link OutputParams} object with the correct audio track
   * (aid) based on the MediaInfo metadata and PMS configuration settings.
   *
   * @param media The MediaInfo metadata for the file.
   * @param params The parameters to populate.
   */
  public static void setAudioOutputParameters(DLNAMediaInfo media, OutputParams params) {
    PmsConfiguration configuration = PMS.getConfiguration(params);
    if (params.aid == null && media != null && media.getFirstAudioTrack() != null) {
      // check for preferred audio
      DLNAMediaAudio dtsTrack = null;
      StringTokenizer st = new StringTokenizer(configuration.getAudioLanguages(), ",");
      while (st.hasMoreTokens()) {
        String lang = st.nextToken().trim();
        LOGGER.trace("Looking for an audio track with lang: " + lang);
        for (DLNAMediaAudio audio : media.getAudioTracksList()) {
          if (audio.matchCode(lang)) {
            params.aid = audio;
            LOGGER.trace("Matched audio track: " + audio);
            return;
          }

          if (dtsTrack == null && audio.isDTS()) {
            dtsTrack = audio;
          }
        }
      }

      // preferred audio not found, take a default audio track, dts first if available
      if (dtsTrack != null) {
        params.aid = dtsTrack;
        LOGGER.trace("Found priority audio track with DTS: " + dtsTrack);
      } else {
        params.aid = media.getAudioTracksList().get(0);
        LOGGER.trace("Chose a default audio track: " + params.aid);
      }
    }
  }
  /**
   * Creates an InputStream based on a URL. This is used while accessing external resources like
   * online radio stations.
   *
   * @param u URL.
   * @param saveOnDisk If true, the file is first downloaded to the temporary folder.
   * @return InputStream that can be used for sending to the media renderer.
   * @throws IOException
   * @see #downloadAndSendBinary(String)
   */
  protected static InputStream downloadAndSend(String u, boolean saveOnDisk) throws IOException {
    URL url = new URL(u);
    File f = null;

    if (saveOnDisk) {
      String host = url.getHost();
      String hostName = convertURLToFileName(host);
      String fileName = url.getFile();
      fileName = convertURLToFileName(fileName);
      File hostDir = new File(PMS.getConfiguration().getTempFolder(), hostName);

      if (!hostDir.isDirectory()) {
        if (!hostDir.mkdir()) {
          LOGGER.debug("Cannot create directory: {}", hostDir.getAbsolutePath());
        }
      }

      f = new File(hostDir, fileName);

      if (f.exists()) {
        return new FileInputStream(f);
      }
    }

    byte[] content = downloadAndSendBinary(u, saveOnDisk, f);
    return new ByteArrayInputStream(content);
  }
  public static boolean isSubtitlesExists(File file, DLNAMediaInfo media, boolean usecache) {
    boolean found = false;
    if (file.exists()) {
      found = browseFolderForSubtitles(file.getParentFile(), file, media, usecache);
    }
    String alternate = PMS.getConfiguration().getAlternateSubtitlesFolder();

    if (isNotBlank(alternate)) { // https://code.google.com/p/ps3mediaserver/issues/detail?id=737#c5
      File subFolder = new File(alternate);

      if (!subFolder.isAbsolute()) {
        subFolder = new File(file.getParent() + "/" + alternate);
        try {
          subFolder = subFolder.getCanonicalFile();
        } catch (IOException e) {
          LOGGER.debug("Caught exception", e);
        }
      }

      if (subFolder.exists()) {
        found = found || browseFolderForSubtitles(subFolder, file, media, usecache);
      }
    }

    return found;
  }
 @Override
 public ProcessWrapper launchTranscode(
     String fileName, DLNAResource dlna, DLNAMediaInfo media, OutputParams params)
     throws IOException {
   params.maxBufferSize = PMS.getConfiguration().getMaxAudioBuffer();
   params.waitbeforestart = 2000;
   params.manageFastStart();
   String args[] = args();
   if (params.mediaRenderer.isTranscodeToMP3()) {
     args = new String[] {"-f", "mp3", "-ar", "48000", "-ab", "320000"};
   }
   if (params.mediaRenderer.isTranscodeToWAV()) {
     args = new String[] {"-f", "wav", "-ar", "48000"};
   }
   if (params.mediaRenderer.isTranscodeAudioTo441()) {
     args[3] = "44100";
   }
   if (!configuration.isAudioResample()) {
     args[2] = "-vn";
     args[3] = "-vn";
   }
   if (params.mediaRenderer.isTranscodeAudioTo441()) {
     args[3] = "44100";
   }
   return getFFMpegTranscode(fileName, dlna, media, params, args);
 }
 public static String getMsgString(String key, HttpExchange t) {
   if (PMS.getConfiguration().useWebLang()) {
     String lang = getFirstSupportedLanguage(t);
     if (!lang.isEmpty()) {
       return Messages.getString(key, Locale.forLanguageTag(lang));
     }
   }
   return Messages.getString(key);
 }
Exemple #9
0
 public static boolean downloadText(InputStream in, File f) throws Exception {
   PmsConfiguration configuration = PMS.getConfiguration();
   // String subtitleQuality = config.getMencoderVobsubSubtitleQuality();
   String subcp = configuration.getMencoderSubCp();
   OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(f), subcp);
   InputStreamReader inn = new InputStreamReader(in);
   char[] buf = new char[4096];
   int len;
   while ((len = inn.read(buf)) != -1) out.write(buf, 0, len);
   out.flush();
   out.close();
   in.close();
   return true;
 }
  public void checkUnicode() {
    if (file != null && file.exists() && file.length() > 3) {
      FileInputStream fis = null;
      try {
        int is_file_unicode = 0;

        fis = new FileInputStream(file);
        int b1 = fis.read();
        int b2 = fis.read();
        int b3 = fis.read();
        if (b1 == 255 && b2 == 254) {
          is_file_unicode = 1;
        } else if (b1 == 254 && b2 == 255) {
          is_file_unicode = 2;
        } else if (b1 == 239 && b2 == 187 && b3 == 191) {
          is_file_utf8 = true;
        }

        // MPlayer doesn't handle UTF-16 encoded subs
        if (is_file_unicode > 0) {
          is_file_utf8 = true;
          utf8_file = new File(PMS.getConfiguration().getTempFolder(), "utf8_" + file.getName());
          if (!utf8_file.exists()) {
            InputStreamReader r =
                new InputStreamReader(
                    new FileInputStream(file), is_file_unicode == 1 ? "UTF-16" : "UTF-16BE");
            OutputStreamWriter osw =
                new OutputStreamWriter(new FileOutputStream(utf8_file), "UTF-8");
            int c;
            while ((c = r.read()) != -1) {
              osw.write(c);
            }
            osw.close();
            r.close();
          }
        }
      } catch (IOException e) {
        LOGGER.error(null, e);
      } finally {
        if (fis != null) {
          try {
            fis.close();
          } catch (IOException e) {
            LOGGER.debug("Caught exception", e);
          }
        }
      }
    }
  }
  protected ProcessWrapperImpl getFFMpegTranscode(
      String fileName, DLNAResource dlna, DLNAMediaInfo media, OutputParams params)
      throws IOException {
    PmsConfiguration configuration = PMS.getConfiguration();
    String ffmpegAlternativePath = configuration.getFfmpegAlternativePath();
    List<String> cmdList = new ArrayList<String>();

    if (ffmpegAlternativePath != null && ffmpegAlternativePath.length() > 0) {
      cmdList.add(ffmpegAlternativePath);
    } else {
      cmdList.add(executable());
    }

    if (params.timeseek > 0) {
      cmdList.add("-ss");
      cmdList.add("" + params.timeseek);
    }

    cmdList.add("-i");
    cmdList.add(fileName);

    for (String arg : args()) {
      cmdList.add(arg);
    }

    String[] ffmpegSettings = StringUtils.split(configuration.getFfmpegSettings());

    if (ffmpegSettings != null) {
      for (String option : ffmpegSettings) {
        cmdList.add(option);
      }
    }

    cmdList.add("pipe:");
    String[] cmdArray = new String[cmdList.size()];
    cmdList.toArray(cmdArray);

    cmdArray = finalizeTranscoderArgs(this, fileName, dlna, media, params, cmdArray);

    ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params);
    pw.runInNewThread();

    return pw;
  }
Exemple #12
0
 private static String addSubs(String rUrl, String sub) {
   rUrl = append(rUrl, "&subs=", escape(sub));
   // -spuaa 3 -subcp ISO-8859-10 -subfont C:\Windows\Fonts\Arial.ttf -subfont-text-scale 2
   // -subfont-outline 1 -subfont-blur 1 -subpos 90 -quiet -quiet -sid 100 -fps 25 -ofps 25 -sub
   // C:\downloads\Kings Speech.srt -lavdopts fast -mc 0 -noskip -af lavcresample=48000 -srate
   // 48000 -o \\.\pipe\mencoder1299956406082
   PmsConfiguration configuration = PMS.getConfiguration();
   // String subtitleQuality = config.getMencoderVobsubSubtitleQuality();
   String subcp = configuration.getMencoderSubCp();
   rUrl = append(rUrl, "&subcp=", escape(subcp));
   rUrl = append(rUrl, "&subtext=", escape(configuration.getMencoderNoAssScale()));
   rUrl = append(rUrl, "&subout=", escape(configuration.getMencoderNoAssOutline()));
   rUrl = append(rUrl, "&subblur=", escape(configuration.getMencoderNoAssBlur()));
   int subpos = 1;
   try {
     subpos = Integer.parseInt(configuration.getMencoderNoAssSubPos());
   } catch (NumberFormatException n) {
   }
   rUrl = append(rUrl, "&subpos=", String.valueOf(100 - subpos));
   //  rUrl=append(rUrl,"&subdelay=","20000");
   return rUrl;
 }
Exemple #13
0
  public static String servePage(String resource) {
    StringBuilder sb = new StringBuilder();
    sb.append(
        "<html><head><title>"
            + PropertiesUtil.getProjectProperties().get("project.name")
            + " HTML Console</title></head><body>");

    DLNAMediaDatabase database = PMS.get().getDatabase();
    PmsConfiguration configuration = PMS.getConfiguration();
    if (resource.equals("compact") && configuration.getUseCache()) {
      database.compact();
      sb.append("<p align=center><b>Database compacted!</b></p><br>");
    }

    if (resource.equals("scan") && configuration.getUseCache()) {
      if (!database.isScanLibraryRunning()) {
        database.scanLibrary();
      }
      if (database.isScanLibraryRunning()) {
        sb.append(
            "<p align=center><b>Scan in progress! you can also <a href=\"stop\">stop it</a></b></p><br>");
      }
    }

    if (resource.equals("stop") && configuration.getUseCache() && database.isScanLibraryRunning()) {
      sb.append("<p align=center><b>Scan stopped!</b></p><br>");
    }

    sb.append(
        "<p align=center><img src='/images/thumbnail-256.png'><br>"
            + PropertiesUtil.getProjectProperties().get("project.name")
            + " HTML console<br><br>Menu:<br>");
    sb.append("<a href=\"home\">Home</a><br>");
    sb.append("<a href=\"scan\">Scan folders</a><br>");
    sb.append("<a href=\"compact\">Shrink cache database (not recommended)</a>");
    sb.append("</p></body></html>");
    return sb.toString();
  }
 private static int getHW(int cfgVal, int id, int def) {
   if (cfgVal != 0) {
     // if we have a value cfg return that
     return cfgVal;
   }
   String s = PMS.getConfiguration().getWebSize();
   if (StringUtils.isEmpty(s)) {
     // no size string return default
     return def;
   }
   String[] tmp = s.split("x", 2);
   if (tmp.length < 2) {
     // bad format resort to default
     return def;
   }
   try {
     // pick whatever we got
     return Integer.parseInt(tmp[id]);
   } catch (NumberFormatException e) {
     // bad format (again) resort to default
     return def;
   }
 }
/**
 * This class provides methods for creating and maintaining the database where media information is
 * stored. Scanning media and interpreting the data is intensive, so the database is used to cache
 * scanned information to be reused later.
 */
public class DLNAMediaDatabase implements Runnable {
  private static final Logger LOGGER = LoggerFactory.getLogger(DLNAMediaDatabase.class);
  private static final PmsConfiguration configuration = PMS.getConfiguration();

  private String url;
  private String dbDir;
  private String dbName;
  public static final String NONAME = "###";
  private Thread scanner;
  private JdbcConnectionPool cp;
  private int dbCount;

  /**
   * The database version should be incremented when we change anything to do with the database
   * since the last released version.
   */
  private final String latestVersion = "7";

  // Database column sizes
  private final int SIZE_CODECV = 32;
  private final int SIZE_FRAMERATE = 32;
  private final int SIZE_ASPECTRATIO_DVDISO = 32;
  private final int SIZE_ASPECTRATIO_CONTAINER = 5;
  private final int SIZE_ASPECTRATIO_VIDEOTRACK = 5;
  private final int SIZE_AVC_LEVEL = 3;
  private final int SIZE_CONTAINER = 32;
  private final int SIZE_MATRIX_COEFFICIENTS = 16;
  private final int SIZE_MODEL = 128;
  private final int SIZE_MUXINGMODE = 32;
  private final int SIZE_FRAMERATE_MODE = 16;
  private final int SIZE_STEREOSCOPY = 255;
  private final int SIZE_LANG = 3;
  private final int SIZE_TITLE = 255;
  private final int SIZE_SAMPLEFREQ = 16;
  private final int SIZE_CODECA = 32;
  private final int SIZE_ALBUM = 255;
  private final int SIZE_ARTIST = 255;
  private final int SIZE_SONGNAME = 255;
  private final int SIZE_GENRE = 64;

  public DLNAMediaDatabase(String name) {
    dbName = name;
    File profileDirectory = new File(configuration.getProfileDirectory());
    dbDir =
        new File(
                profileDirectory.isDirectory() ? configuration.getProfileDirectory() : null,
                "database")
            .getAbsolutePath();
    url = Constants.START_URL + dbDir + File.separator + dbName;
    LOGGER.debug("Using database URL: " + url);
    LOGGER.info("Using database located at: " + dbDir);

    try {
      Class.forName("org.h2.Driver");
    } catch (ClassNotFoundException e) {
      LOGGER.error(null, e);
    }

    JdbcDataSource ds = new JdbcDataSource();
    ds.setURL(url);
    ds.setUser("sa");
    ds.setPassword("");
    cp = JdbcConnectionPool.create(ds);
  }

  /**
   * Gets the name of the database file
   *
   * @return The filename
   */
  public String getDatabaseFilename() {
    if (dbName == null || dbDir == null) {
      return null;
    } else {
      return dbDir + File.separator + dbName;
    }
  }

  /**
   * Gets a new connection from the connection pool if one is available. If not waits for a free
   * slot until timeout.<br>
   * <br>
   * <strong>Important: Every connection must be closed after use</strong>
   *
   * @return the new connection
   * @throws SQLException
   */
  public Connection getConnection() throws SQLException {
    return cp.getConnection();
  }

  public void init(boolean force) {
    dbCount = -1;
    String version = null;
    Connection conn = null;
    ResultSet rs = null;
    Statement stmt = null;

    try {
      conn = getConnection();
    } catch (SQLException se) {
      final File dbFile = new File(dbDir + File.separator + dbName + ".data.db");
      final File dbDirectory = new File(dbDir);
      if (dbFile.exists()
          || (se.getErrorCode() == 90048)) { // Cache is corrupt or a wrong version, so delete it
        FileUtils.deleteQuietly(dbDirectory);
        if (!dbDirectory.exists()) {
          LOGGER.info(
              "The database has been deleted because it was corrupt or had the wrong version");
        } else {
          if (!net.pms.PMS.isHeadless()) {
            JOptionPane.showMessageDialog(
                SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()),
                String.format(Messages.getString("DLNAMediaDatabase.5"), dbDir),
                Messages.getString("Dialog.Error"),
                JOptionPane.ERROR_MESSAGE);
          }
          LOGGER.error(
              "Damaged cache can't be deleted. Stop the program and delete the folder \""
                  + dbDir
                  + "\" manually");
          PMS.get().getRootFolder(null).stopScan();
          configuration.setUseCache(false);
          return;
        }
      } else {
        LOGGER.error("Database connection error: " + se.getMessage());
        LOGGER.trace("", se);
        RootFolder rootFolder = PMS.get().getRootFolder(null);
        if (rootFolder != null) {
          rootFolder.stopScan();
        }
        configuration.setUseCache(false);
        return;
      }
    } finally {
      close(conn);
    }

    try {
      conn = getConnection();

      stmt = conn.createStatement();
      rs = stmt.executeQuery("SELECT count(*) FROM FILES");
      if (rs.next()) {
        dbCount = rs.getInt(1);
      }
      rs.close();
      stmt.close();

      stmt = conn.createStatement();
      rs = stmt.executeQuery("SELECT VALUE FROM METADATA WHERE KEY = 'VERSION'");
      if (rs.next()) {
        version = rs.getString(1);
      }
    } catch (SQLException se) {
      if (se.getErrorCode()
          != 42102) { // Don't log exception "Table "FILES" not found" which will be corrected in
                      // following step
        LOGGER.error(null, se);
      }
    } finally {
      close(rs);
      close(stmt);
      close(conn);
    }

    // Recreate database if it is not the latest version.
    boolean force_reinit = !latestVersion.equals(version);
    if (force || dbCount == -1 || force_reinit) {
      LOGGER.debug("Database will be (re)initialized");
      try {
        conn = getConnection();
        executeUpdate(conn, "DROP TABLE FILES");
        executeUpdate(conn, "DROP TABLE METADATA");
        executeUpdate(conn, "DROP TABLE REGEXP_RULES");
        executeUpdate(conn, "DROP TABLE AUDIOTRACKS");
        executeUpdate(conn, "DROP TABLE SUBTRACKS");
      } catch (SQLException se) {
        if (se.getErrorCode()
            != 42102) { // Don't log exception "Table "FILES" not found" which will be corrected in
                        // following step
          LOGGER.error(null, se);
        }
      }
      try {
        StringBuilder sb = new StringBuilder();
        sb.append("CREATE TABLE FILES (");
        sb.append("  ID                      INT AUTO_INCREMENT");
        sb.append(", FILENAME                VARCHAR2(1024)   NOT NULL");
        sb.append(", MODIFIED                TIMESTAMP        NOT NULL");
        sb.append(", TYPE                    INT");
        sb.append(", DURATION                DOUBLE");
        sb.append(", BITRATE                 INT");
        sb.append(", WIDTH                   INT");
        sb.append(", HEIGHT                  INT");
        sb.append(", SIZE                    NUMERIC");
        sb.append(", CODECV                  VARCHAR2(").append(SIZE_CODECV).append(')');
        sb.append(", FRAMERATE               VARCHAR2(").append(SIZE_FRAMERATE).append(')');
        sb.append(", ASPECT                  VARCHAR2(")
            .append(SIZE_ASPECTRATIO_DVDISO)
            .append(')');
        sb.append(", ASPECTRATIOCONTAINER    VARCHAR2(")
            .append(SIZE_ASPECTRATIO_CONTAINER)
            .append(')');
        sb.append(", ASPECTRATIOVIDEOTRACK   VARCHAR2(")
            .append(SIZE_ASPECTRATIO_VIDEOTRACK)
            .append(')');
        sb.append(", REFRAMES                TINYINT");
        sb.append(", AVCLEVEL                VARCHAR2(").append(SIZE_AVC_LEVEL).append(')');
        sb.append(", BITSPERPIXEL            INT");
        sb.append(", THUMB                   BINARY");
        sb.append(", CONTAINER               VARCHAR2(").append(SIZE_CONTAINER).append(')');
        sb.append(", MODEL                   VARCHAR2(").append(SIZE_MODEL).append(')');
        sb.append(", EXPOSURE                INT");
        sb.append(", ORIENTATION             INT");
        sb.append(", ISO                     INT");
        sb.append(", MUXINGMODE              VARCHAR2(").append(SIZE_MUXINGMODE).append(')');
        sb.append(", FRAMERATEMODE           VARCHAR2(").append(SIZE_FRAMERATE_MODE).append(')');
        sb.append(", STEREOSCOPY             VARCHAR2(").append(SIZE_STEREOSCOPY).append(')');
        sb.append(", MATRIXCOEFFICIENTS      VARCHAR2(")
            .append(SIZE_MATRIX_COEFFICIENTS)
            .append(')');
        sb.append(", TITLECONTAINER          VARCHAR2(").append(SIZE_TITLE).append(')');
        sb.append(", TITLEVIDEOTRACK         VARCHAR2(").append(SIZE_TITLE).append(')');
        sb.append(", VIDEOTRACKCOUNT         INT");
        sb.append(", IMAGECOUNT              INT");
        sb.append(", BITDEPTH                INT");
        sb.append(", constraint PK1 primary key (FILENAME, MODIFIED, ID))");
        executeUpdate(conn, sb.toString());
        sb = new StringBuilder();
        sb.append("CREATE TABLE AUDIOTRACKS (");
        sb.append("  FILEID            INT              NOT NULL");
        sb.append(", ID                INT              NOT NULL");
        sb.append(", LANG              VARCHAR2(").append(SIZE_LANG).append(')');
        sb.append(", TITLE             VARCHAR2(").append(SIZE_TITLE).append(')');
        sb.append(", NRAUDIOCHANNELS   NUMERIC");
        sb.append(", SAMPLEFREQ        VARCHAR2(").append(SIZE_SAMPLEFREQ).append(')');
        sb.append(", CODECA            VARCHAR2(").append(SIZE_CODECA).append(')');
        sb.append(", BITSPERSAMPLE     INT");
        sb.append(", ALBUM             VARCHAR2(").append(SIZE_ALBUM).append(')');
        sb.append(", ARTIST            VARCHAR2(").append(SIZE_ARTIST).append(')');
        sb.append(", SONGNAME          VARCHAR2(").append(SIZE_SONGNAME).append(')');
        sb.append(", GENRE             VARCHAR2(").append(SIZE_GENRE).append(')');
        sb.append(", YEAR              INT");
        sb.append(", TRACK             INT");
        sb.append(", DELAY             INT");
        sb.append(", MUXINGMODE        VARCHAR2(").append(SIZE_MUXINGMODE).append(')');
        sb.append(", BITRATE           INT");
        sb.append(", constraint PKAUDIO primary key (FILEID, ID))");
        executeUpdate(conn, sb.toString());
        sb = new StringBuilder();
        sb.append("CREATE TABLE SUBTRACKS (");
        sb.append("  FILEID   INT              NOT NULL");
        sb.append(", ID       INT              NOT NULL");
        sb.append(", LANG     VARCHAR2(").append(SIZE_LANG).append(')');
        sb.append(", TITLE    VARCHAR2(").append(SIZE_TITLE).append(')');
        sb.append(", TYPE     INT");
        sb.append(", constraint PKSUB primary key (FILEID, ID))");

        executeUpdate(conn, sb.toString());
        executeUpdate(
            conn,
            "CREATE TABLE METADATA (KEY VARCHAR2(255) NOT NULL, VALUE VARCHAR2(255) NOT NULL)");
        executeUpdate(conn, "INSERT INTO METADATA VALUES ('VERSION', '" + latestVersion + "')");
        executeUpdate(conn, "CREATE INDEX IDXARTIST on AUDIOTRACKS (ARTIST asc);");
        executeUpdate(conn, "CREATE INDEX IDXALBUM on AUDIOTRACKS (ALBUM asc);");
        executeUpdate(conn, "CREATE INDEX IDXGENRE on AUDIOTRACKS (GENRE asc);");
        executeUpdate(conn, "CREATE INDEX IDXYEAR on AUDIOTRACKS (YEAR asc);");
        executeUpdate(
            conn,
            "CREATE TABLE REGEXP_RULES ( ID VARCHAR2(255) PRIMARY KEY, RULE VARCHAR2(255), ORDR NUMERIC);");
        executeUpdate(conn, "INSERT INTO REGEXP_RULES VALUES ( '###', '(?i)^\\W.+', 0 );");
        executeUpdate(conn, "INSERT INTO REGEXP_RULES VALUES ( '0-9', '(?i)^\\d.+', 1 );");

        // Retrieve the alphabet property value and split it
        String[] chars = Messages.getString("DLNAMediaDatabase.1").split(",");

        for (int i = 0; i < chars.length; i++) {
          // Create regexp rules for characters with a sort order based on the property value
          executeUpdate(
              conn,
              "INSERT INTO REGEXP_RULES VALUES ( '"
                  + chars[i]
                  + "', '(?i)^"
                  + chars[i]
                  + ".+', "
                  + (i + 2)
                  + " );");
        }

        LOGGER.debug("Database initialized");
      } catch (SQLException se) {
        LOGGER.info("Error in table creation: " + se.getMessage());
      } finally {
        close(conn);
      }
    } else {
      LOGGER.debug("Database file count: " + dbCount);
      LOGGER.debug("Database version: " + latestVersion);
    }
  }

  private void executeUpdate(Connection conn, String sql) throws SQLException {
    if (conn != null) {
      try (Statement stmt = conn.createStatement()) {
        stmt.executeUpdate(sql);
      }
    }
  }

  public boolean isDataExists(String name, long modified) {
    boolean found = false;
    Connection conn = null;
    ResultSet rs = null;
    PreparedStatement stmt = null;
    try {
      conn = getConnection();
      stmt = conn.prepareStatement("SELECT * FROM FILES WHERE FILENAME = ? AND MODIFIED = ?");
      stmt.setString(1, name);
      stmt.setTimestamp(2, new Timestamp(modified));
      rs = stmt.executeQuery();
      while (rs.next()) {
        found = true;
      }
    } catch (SQLException se) {
      LOGGER.error(null, se);
      return false;
    } finally {
      close(rs);
      close(stmt);
      close(conn);
    }
    return found;
  }

  public ArrayList<DLNAMediaInfo> getData(String name, long modified) {
    ArrayList<DLNAMediaInfo> list = new ArrayList<>();
    Connection conn = null;
    ResultSet rs = null;
    PreparedStatement stmt = null;
    try {
      conn = getConnection();
      stmt = conn.prepareStatement("SELECT * FROM FILES WHERE FILENAME = ? AND MODIFIED = ?");
      stmt.setString(1, name);
      stmt.setTimestamp(2, new Timestamp(modified));
      rs = stmt.executeQuery();
      while (rs.next()) {
        DLNAMediaInfo media = new DLNAMediaInfo();
        int id = rs.getInt("ID");
        media.setDuration(toDouble(rs, "DURATION"));
        media.setBitrate(rs.getInt("BITRATE"));
        media.setWidth(rs.getInt("WIDTH"));
        media.setHeight(rs.getInt("HEIGHT"));
        media.setSize(rs.getLong("SIZE"));
        media.setCodecV(rs.getString("CODECV"));
        media.setFrameRate(rs.getString("FRAMERATE"));
        media.setAspectRatioDvdIso(rs.getString("ASPECT"));
        media.setAspectRatioContainer(rs.getString("ASPECTRATIOCONTAINER"));
        media.setAspectRatioVideoTrack(rs.getString("ASPECTRATIOVIDEOTRACK"));
        media.setReferenceFrameCount(rs.getByte("REFRAMES"));
        media.setAvcLevel(rs.getString("AVCLEVEL"));
        media.setBitsPerPixel(rs.getInt("BITSPERPIXEL"));
        media.setThumb(rs.getBytes("THUMB"));
        media.setContainer(rs.getString("CONTAINER"));
        media.setModel(rs.getString("MODEL"));
        if (media.getModel() != null && !FormatConfiguration.JPG.equals(media.getContainer())) {
          media.setExtrasAsString(media.getModel());
        }
        media.setExposure(rs.getInt("EXPOSURE"));
        media.setOrientation(rs.getInt("ORIENTATION"));
        media.setIso(rs.getInt("ISO"));
        media.setMuxingMode(rs.getString("MUXINGMODE"));
        media.setFrameRateMode(rs.getString("FRAMERATEMODE"));
        media.setStereoscopy(rs.getString("STEREOSCOPY"));
        media.setMatrixCoefficients(rs.getString("MATRIXCOEFFICIENTS"));
        media.setFileTitleFromMetadata(rs.getString("TITLECONTAINER"));
        media.setVideoTrackTitleFromMetadata(rs.getString("TITLEVIDEOTRACK"));
        media.setVideoTrackCount(rs.getInt("VIDEOTRACKCOUNT"));
        media.setImageCount(rs.getInt("IMAGECOUNT"));
        media.setVideoBitDepth(rs.getInt("BITDEPTH"));
        media.setMediaparsed(true);
        ResultSet subrs;
        try (PreparedStatement audios =
            conn.prepareStatement("SELECT * FROM AUDIOTRACKS WHERE FILEID = ?")) {
          audios.setInt(1, id);
          subrs = audios.executeQuery();
          while (subrs.next()) {
            DLNAMediaAudio audio = new DLNAMediaAudio();
            audio.setId(subrs.getInt("ID"));
            audio.setLang(subrs.getString("LANG"));
            audio.setAudioTrackTitleFromMetadata(subrs.getString("TITLE"));
            audio.getAudioProperties().setNumberOfChannels(subrs.getInt("NRAUDIOCHANNELS"));
            audio.setSampleFrequency(subrs.getString("SAMPLEFREQ"));
            audio.setCodecA(subrs.getString("CODECA"));
            audio.setBitsperSample(subrs.getInt("BITSPERSAMPLE"));
            audio.setAlbum(subrs.getString("ALBUM"));
            audio.setArtist(subrs.getString("ARTIST"));
            audio.setSongname(subrs.getString("SONGNAME"));
            audio.setGenre(subrs.getString("GENRE"));
            audio.setYear(subrs.getInt("YEAR"));
            audio.setTrack(subrs.getInt("TRACK"));
            audio.getAudioProperties().setAudioDelay(subrs.getInt("DELAY"));
            audio.setMuxingModeAudio(subrs.getString("MUXINGMODE"));
            audio.setBitRate(subrs.getInt("BITRATE"));
            media.getAudioTracksList().add(audio);
          }
          subrs.close();
        }
        try (PreparedStatement subs =
            conn.prepareStatement("SELECT * FROM SUBTRACKS WHERE FILEID = ?")) {
          subs.setInt(1, id);
          subrs = subs.executeQuery();
          while (subrs.next()) {
            DLNAMediaSubtitle sub = new DLNAMediaSubtitle();
            sub.setId(subrs.getInt("ID"));
            sub.setLang(subrs.getString("LANG"));
            sub.setSubtitlesTrackTitleFromMetadata(subrs.getString("TITLE"));
            sub.setType(SubtitleType.valueOfStableIndex(subrs.getInt("TYPE")));
            media.getSubtitleTracksList().add(sub);
          }
          subrs.close();
        }

        list.add(media);
      }
    } catch (SQLException se) {
      LOGGER.error(null, se);
      return null;
    } finally {
      close(rs);
      close(stmt);
      close(conn);
    }
    return list;
  }

  private Double toDouble(ResultSet rs, String column) throws SQLException {
    Object obj = rs.getObject(column);
    if (obj instanceof Double) {
      return (Double) obj;
    }
    return null;
  }

  public synchronized void insertData(String name, long modified, int type, DLNAMediaInfo media) {
    Connection conn = null;
    PreparedStatement ps = null;
    try {
      conn = getConnection();
      ps =
          conn.prepareStatement(
              "INSERT INTO FILES(FILENAME, MODIFIED, TYPE, DURATION, BITRATE, WIDTH, HEIGHT, SIZE, CODECV, "
                  + "FRAMERATE, ASPECT, ASPECTRATIOCONTAINER, ASPECTRATIOVIDEOTRACK, REFRAMES, AVCLEVEL, BITSPERPIXEL, "
                  + "THUMB, CONTAINER, MODEL, EXPOSURE, ORIENTATION, ISO, MUXINGMODE, FRAMERATEMODE, STEREOSCOPY, "
                  + "MATRIXCOEFFICIENTS, TITLECONTAINER, TITLEVIDEOTRACK, VIDEOTRACKCOUNT, IMAGECOUNT, BITDEPTH) VALUES "
                  + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
      ps.setString(1, name);
      ps.setTimestamp(2, new Timestamp(modified));
      ps.setInt(3, type);
      if (media != null) {
        if (media.getDuration() != null) {
          ps.setDouble(4, media.getDurationInSeconds());
        } else {
          ps.setNull(4, Types.DOUBLE);
        }

        int databaseBitrate = 0;
        if (type != Format.IMAGE) {
          databaseBitrate = media.getBitrate();
          if (databaseBitrate == 0) {
            LOGGER.debug("Could not parse the bitrate from: " + name);
          }
        }
        ps.setInt(5, databaseBitrate);

        ps.setInt(6, media.getWidth());
        ps.setInt(7, media.getHeight());
        ps.setLong(8, media.getSize());
        ps.setString(9, left(media.getCodecV(), SIZE_CODECV));
        ps.setString(10, left(media.getFrameRate(), SIZE_FRAMERATE));
        ps.setString(11, left(media.getAspectRatioDvdIso(), SIZE_ASPECTRATIO_DVDISO));
        ps.setString(12, left(media.getAspectRatioContainer(), SIZE_ASPECTRATIO_CONTAINER));
        ps.setString(13, left(media.getAspectRatioVideoTrack(), SIZE_ASPECTRATIO_VIDEOTRACK));
        ps.setByte(14, media.getReferenceFrameCount());
        ps.setString(15, left(media.getAvcLevel(), SIZE_AVC_LEVEL));
        ps.setInt(16, media.getBitsPerPixel());
        ps.setBytes(17, media.getThumb());
        ps.setString(18, left(media.getContainer(), SIZE_CONTAINER));
        if (media.getExtras() != null) {
          ps.setString(19, left(media.getExtrasAsString(), SIZE_MODEL));
        } else {
          ps.setString(19, left(media.getModel(), SIZE_MODEL));
        }
        ps.setInt(20, media.getExposure());
        ps.setInt(21, media.getOrientation());
        ps.setInt(22, media.getIso());
        ps.setString(23, left(media.getMuxingModeAudio(), SIZE_MUXINGMODE));
        ps.setString(24, left(media.getFrameRateMode(), SIZE_FRAMERATE_MODE));
        ps.setString(25, left(media.getStereoscopy(), SIZE_STEREOSCOPY));
        ps.setString(26, left(media.getMatrixCoefficients(), SIZE_MATRIX_COEFFICIENTS));
        ps.setString(27, left(media.getFileTitleFromMetadata(), SIZE_TITLE));
        ps.setString(28, left(media.getVideoTrackTitleFromMetadata(), SIZE_TITLE));
        ps.setInt(29, media.getVideoTrackCount());
        ps.setInt(30, media.getImageCount());
        ps.setInt(31, media.getVideoBitDepth());
      } else {
        ps.setString(4, null);
        ps.setInt(5, 0);
        ps.setInt(6, 0);
        ps.setInt(7, 0);
        ps.setLong(8, 0);
        ps.setString(9, null);
        ps.setString(10, null);
        ps.setString(11, null);
        ps.setString(12, null);
        ps.setString(13, null);
        ps.setByte(14, (byte) -1);
        ps.setString(15, null);
        ps.setInt(16, 0);
        ps.setBytes(17, null);
        ps.setString(18, null);
        ps.setString(19, null);
        ps.setInt(20, 0);
        ps.setInt(21, 0);
        ps.setInt(22, 0);
        ps.setString(23, null);
        ps.setString(24, null);
        ps.setString(25, null);
        ps.setString(26, null);
        ps.setString(27, null);
        ps.setString(28, null);
        ps.setInt(29, 0);
        ps.setInt(30, 0);
        ps.setInt(31, 0);
      }
      ps.executeUpdate();
      int id;
      try (ResultSet rs = ps.getGeneratedKeys()) {
        id = -1;
        while (rs.next()) {
          id = rs.getInt(1);
        }
      }
      if (media != null && id > -1) {
        PreparedStatement insert = null;
        if (media.getAudioTracksList().size() > 0) {
          insert =
              conn.prepareStatement(
                  "INSERT INTO AUDIOTRACKS VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
          for (DLNAMediaAudio audio : media.getAudioTracksList()) {
            insert.clearParameters();
            insert.setInt(1, id);
            insert.setInt(2, audio.getId());
            insert.setString(3, left(audio.getLang(), SIZE_LANG));
            insert.setString(4, left(audio.getAudioTrackTitleFromMetadata(), SIZE_TITLE));
            insert.setInt(5, audio.getAudioProperties().getNumberOfChannels());
            insert.setString(6, left(audio.getSampleFrequency(), SIZE_SAMPLEFREQ));
            insert.setString(7, left(audio.getCodecA(), SIZE_CODECA));
            insert.setInt(8, audio.getBitsperSample());
            insert.setString(9, left(trimToEmpty(audio.getAlbum()), SIZE_ALBUM));
            insert.setString(10, left(trimToEmpty(audio.getArtist()), SIZE_ARTIST));
            insert.setString(11, left(trimToEmpty(audio.getSongname()), SIZE_SONGNAME));
            insert.setString(12, left(trimToEmpty(audio.getGenre()), SIZE_GENRE));
            insert.setInt(13, audio.getYear());
            insert.setInt(14, audio.getTrack());
            insert.setInt(15, audio.getAudioProperties().getAudioDelay());
            insert.setString(16, left(trimToEmpty(audio.getMuxingModeAudio()), SIZE_MUXINGMODE));
            insert.setInt(17, audio.getBitRate());

            try {
              insert.executeUpdate();
            } catch (SQLException e) {
              if (e.getErrorCode() == 23505) {
                LOGGER.debug(
                    "A duplicate key error occurred while trying to store the following file's audio information in the database: "
                        + name);
              } else {
                LOGGER.debug(
                    "An error occurred while trying to store the following file's audio information in the database: "
                        + name);
              }
              LOGGER.debug("The error given by jdbc was: " + e);
            }
          }
        }

        if (media.getSubtitleTracksList().size() > 0) {
          insert = conn.prepareStatement("INSERT INTO SUBTRACKS VALUES (?, ?, ?, ?, ?)");
          for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
            if (sub.getExternalFile() == null) { // no save of external subtitles
              insert.clearParameters();
              insert.setInt(1, id);
              insert.setInt(2, sub.getId());
              insert.setString(3, left(sub.getLang(), SIZE_LANG));
              insert.setString(4, left(sub.getSubtitlesTrackTitleFromMetadata(), SIZE_TITLE));
              insert.setInt(5, sub.getType().getStableIndex());
              try {
                insert.executeUpdate();
              } catch (SQLException e) {
                if (e.getErrorCode() == 23505) {
                  LOGGER.debug(
                      "A duplicate key error occurred while trying to store the following file's subtitle information in the database: "
                          + name);
                } else {
                  LOGGER.debug(
                      "An error occurred while trying to store the following file's subtitle information in the database: "
                          + name);
                }
                LOGGER.debug("The error given by jdbc was: " + e);
              }
            }
          }
        }

        close(insert);
      }
    } catch (SQLException se) {
      if (se.getErrorCode() == 23505) {
        LOGGER.debug(
            "Duplicate key while inserting this entry: "
                + name
                + " into the database: "
                + se.getMessage());
      } else {
        LOGGER.error(null, se);
      }
    } finally {
      close(ps);
      close(conn);
    }
  }

  public synchronized void updateThumbnail(
      String name, long modified, int type, DLNAMediaInfo media) {
    Connection conn = null;
    PreparedStatement ps = null;
    try {
      conn = getConnection();
      ps = conn.prepareStatement("UPDATE FILES SET THUMB = ? WHERE FILENAME = ? AND MODIFIED = ?");
      ps.setString(2, name);
      ps.setTimestamp(3, new Timestamp(modified));
      if (media != null) {
        ps.setBytes(1, media.getThumb());
      } else {
        ps.setNull(1, Types.BINARY);
      }
      ps.executeUpdate();
    } catch (SQLException se) {
      if (se.getErrorCode() == 23001) {
        LOGGER.debug(
            "Duplicate key while inserting this entry: "
                + name
                + " into the database: "
                + se.getMessage());
      } else {
        LOGGER.error(null, se);
      }
    } finally {
      close(ps);
      close(conn);
    }
  }

  public ArrayList<String> getStrings(String sql) {
    ArrayList<String> list = new ArrayList<>();
    Connection conn = null;
    ResultSet rs = null;
    PreparedStatement ps = null;
    try {
      conn = getConnection();
      ps = conn.prepareStatement(sql);
      rs = ps.executeQuery();
      while (rs.next()) {
        String str = rs.getString(1);
        if (isBlank(str)) {
          if (!list.contains(NONAME)) {
            list.add(NONAME);
          }
        } else if (!list.contains(str)) {
          list.add(str);
        }
      }
    } catch (SQLException se) {
      LOGGER.error(null, se);
      return null;
    } finally {
      close(rs);
      close(ps);
      close(conn);
    }
    return list;
  }

  public void cleanup() {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    try {
      conn = getConnection();
      ps = conn.prepareStatement("SELECT COUNT(*) FROM FILES");
      rs = ps.executeQuery();
      dbCount = 0;

      if (rs.next()) {
        dbCount = rs.getInt(1);
      }

      rs.close();
      ps.close();
      PMS.get().getFrame().setStatusLine(Messages.getString("DLNAMediaDatabase.2") + " 0%");
      int i = 0;
      int oldpercent = 0;

      if (dbCount > 0) {
        ps =
            conn.prepareStatement(
                "SELECT FILENAME, MODIFIED, ID FROM FILES",
                ResultSet.TYPE_FORWARD_ONLY,
                ResultSet.CONCUR_UPDATABLE);
        rs = ps.executeQuery();
        while (rs.next()) {
          String filename = rs.getString("FILENAME");
          long modified = rs.getTimestamp("MODIFIED").getTime();
          File file = new File(filename);
          if (!file.exists() || file.lastModified() != modified) {
            rs.deleteRow();
          }
          i++;
          int newpercent = i * 100 / dbCount;
          if (newpercent > oldpercent) {
            PMS.get()
                .getFrame()
                .setStatusLine(Messages.getString("DLNAMediaDatabase.2") + newpercent + "%");
            oldpercent = newpercent;
          }
        }
      }
    } catch (SQLException se) {
      LOGGER.error(null, se);
    } finally {
      close(rs);
      close(ps);
      close(conn);
    }
  }

  public ArrayList<File> getFiles(String sql) {
    ArrayList<File> list = new ArrayList<>();
    Connection conn = null;
    ResultSet rs = null;
    PreparedStatement ps = null;
    try {
      conn = getConnection();
      ps =
          conn.prepareStatement(
              sql.toLowerCase().startsWith("select")
                  ? sql
                  : ("SELECT FILENAME, MODIFIED FROM FILES WHERE " + sql));
      rs = ps.executeQuery();
      while (rs.next()) {
        String filename = rs.getString("FILENAME");
        long modified = rs.getTimestamp("MODIFIED").getTime();
        File file = new File(filename);
        if (file.exists() && file.lastModified() == modified) {
          list.add(file);
        }
      }
    } catch (SQLException se) {
      LOGGER.error(null, se);
      return null;
    } finally {
      close(rs);
      close(ps);
      close(conn);
    }
    return list;
  }

  private void close(ResultSet rs) {
    try {
      if (rs != null) {
        rs.close();
      }
    } catch (SQLException e) {
      LOGGER.error("error during closing:" + e.getMessage(), e);
    }
  }

  private void close(Statement ps) {
    try {
      if (ps != null) {
        ps.close();
      }
    } catch (SQLException e) {
      LOGGER.error("error during closing:" + e.getMessage(), e);
    }
  }

  private void close(Connection conn) {
    try {
      if (conn != null) {
        conn.close();
      }
    } catch (SQLException e) {
      LOGGER.error("error during closing:" + e.getMessage(), e);
    }
  }

  public boolean isScanLibraryRunning() {
    return scanner != null && scanner.isAlive();
  }

  public void scanLibrary() {
    if (isScanLibraryRunning()) {
      LOGGER.info(Messages.getString("NetworkTab.70"));
    } else {
      scanner = new Thread(this, "Library Scanner");
      scanner.start();
    }
  }

  public void stopScanLibrary() {
    if (isScanLibraryRunning()) {
      PMS.get().getRootFolder(null).stopScan();
    }
  }

  @Override
  public void run() {
    try {
      PMS.get().getRootFolder(null).scan();
    } catch (Exception e) {
      LOGGER.error("Unhandled exception during library scan: {}", e.getMessage());
      LOGGER.trace("", e);
    }
  }
}
  /**
   * Construct a proper HTTP response to a received request. After the response has been created, it
   * is sent and the resulting {@link ChannelFuture} object is returned. See <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">RFC-2616</a> for HTTP header
   * field definitions.
   *
   * @param output The {@link HttpResponse} object that will be used to construct the response.
   * @param e The {@link MessageEvent} object used to communicate with the client that sent the
   *     request.
   * @param close Set to true to close the channel after sending the response. By default the
   *     channel is not closed after sending.
   * @param startStopListenerDelegate The {@link StartStopListenerDelegate} object that is used to
   *     notify plugins that the {@link DLNAResource} is about to start playing.
   * @return The {@link ChannelFuture} object via which the response was sent.
   * @throws IOException
   */
  public ChannelFuture answer(
      HttpResponse output,
      MessageEvent e,
      final boolean close,
      final StartStopListenerDelegate startStopListenerDelegate)
      throws IOException {
    ChannelFuture future = null;
    long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit
    StringBuilder response = new StringBuilder();
    DLNAResource dlna = null;
    boolean xbox = mediaRenderer.isXBOX();

    // Samsung 2012 TVs have a problematic preceding slash that needs to be removed.
    if (argument.startsWith("/")) {
      LOGGER.trace("Stripping preceding slash from: " + argument);
      argument = argument.substring(1);
    }

    if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) {
      // Request to output a page to the HTLM console.
      output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html");
      response.append(HTMLConsole.servePage(argument.substring(8)));
    } else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) {
      // Request to retrieve a file

      // Extract the resource id from the argument string.
      String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/"));

      // Some clients escape the separators in their request, unescape them.
      id = id.replace("%24", "$");

      // Retrieve the DLNAresource itself.
      List<DLNAResource> files =
          PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer);

      if (transferMode != null) {
        output.setHeader("TransferMode.DLNA.ORG", transferMode);
      }

      if (files.size() == 1) {
        // DNLAresource was found.
        dlna = files.get(0);
        String fileName = argument.substring(argument.lastIndexOf("/") + 1);

        if (fileName.startsWith("thumbnail0000")) {
          // This is a request for a thumbnail file.
          output.setHeader(HttpHeaders.Names.CONTENT_TYPE, dlna.getThumbnailContentType());
          output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
          output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT");
          output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");

          if (mediaRenderer.isMediaParserV2()) {
            dlna.checkThumbnail();
          }

          inputStream = dlna.getThumbnailInputStream();
        } else if (fileName.indexOf("subtitle0000") > -1) {
          // This is a request for a subtitle file
          output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain");
          output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT");
          List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitlesCodes();

          if (subs != null && !subs.isEmpty()) {
            // TODO: maybe loop subs to get the requested subtitle type instead of using the first
            // one
            DLNAMediaSubtitle sub = subs.get(0);
            inputStream = new java.io.FileInputStream(sub.getFile());
          }
        } else {
          // This is a request for a regular file.

          // If range has not been initialized yet and the DLNAResource has its
          // own start and end defined, initialize range with those values before
          // requesting the input stream.
          Range.Time splitRange = dlna.getSplitRange();

          if (range.getStart() == null && splitRange.getStart() != null) {
            range.setStart(splitRange.getStart());
          }

          if (range.getEnd() == null && splitRange.getEnd() != null) {
            range.setEnd(splitRange.getEnd());
          }

          inputStream =
              dlna.getInputStream(
                  Range.create(lowRange, highRange, range.getStart(), range.getEnd()),
                  mediaRenderer);

          // Some renderers (like Samsung devices) allow a custom header for a subtitle URL
          String subtitleHttpHeader = mediaRenderer.getSubtitleHttpHeader();

          if (subtitleHttpHeader != null && !"".equals(subtitleHttpHeader)) {
            // Device allows a custom subtitle HTTP header; construct it
            List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitlesCodes();

            if (subs != null && !subs.isEmpty()) {
              DLNAMediaSubtitle sub = subs.get(0);

              int type = sub.getType();

              if (type < DLNAMediaSubtitle.subExtensions.length) {
                String strType = DLNAMediaSubtitle.subExtensions[type - 1];
                String subtitleUrl =
                    "http://"
                        + PMS.get().getServer().getHost()
                        + ':'
                        + PMS.get().getServer().getPort()
                        + "/get/"
                        + id
                        + "/subtitle0000."
                        + strType;
                output.setHeader(subtitleHttpHeader, subtitleUrl);
              }
            }
          }

          String name = dlna.getDisplayName(mediaRenderer);

          if (inputStream == null) {
            // No inputStream indicates that transcoding / remuxing probably crashed.
            LOGGER.error("There is no inputstream to return for " + name);
          } else {
            // Notify plugins that the DLNAresource is about to start playing
            startStopListenerDelegate.start(dlna);

            // Try to determine the content type of the file
            String rendererMimeType = getRendererMimeType(dlna.mimeType(), mediaRenderer);

            if (rendererMimeType != null && !"".equals(rendererMimeType)) {
              output.setHeader(HttpHeaders.Names.CONTENT_TYPE, rendererMimeType);
            }

            final DLNAMediaInfo media = dlna.getMedia();
            if (media != null) {
              if (StringUtils.isNotBlank(media.getContainer())) {
                name += " [container: " + media.getContainer() + "]";
              }

              if (StringUtils.isNotBlank(media.getCodecV())) {
                name += " [video: " + media.getCodecV() + "]";
              }
            }

            PMS.get().getFrame().setStatusLine("Serving " + name);

            // Response generation:
            // We use -1 for arithmetic convenience but don't send it as a value.
            // If Content-Length < 0 we omit it, for Content-Range we use '*' to signify
            // unspecified.

            boolean chunked = mediaRenderer.isChunkedTransfer();

            // Determine the total size. Note: when transcoding the length is
            // not known in advance, so DLNAMediaInfo.TRANS_SIZE will be returned instead.

            long totalsize = dlna.length(mediaRenderer);

            if (chunked && totalsize == DLNAMediaInfo.TRANS_SIZE) {
              // In chunked mode we try to avoid arbitrary values.
              totalsize = -1;
            }

            long remaining = totalsize - lowRange;
            long requested = highRange - lowRange;

            if (requested != 0) {
              // Determine the range (i.e. smaller of known or requested bytes)
              long bytes = remaining > -1 ? remaining : inputStream.available();

              if (requested > 0 && bytes > requested) {
                bytes = requested + 1;
              }

              // Calculate the corresponding highRange (this is usually redundant).
              highRange = lowRange + bytes - (bytes > 0 ? 1 : 0);

              LOGGER.trace(
                  (chunked ? "Using chunked response. " : "") + "Sending " + bytes + " bytes.");

              output.setHeader(
                  HttpHeaders.Names.CONTENT_RANGE,
                  "bytes "
                      + lowRange
                      + "-"
                      + (highRange > -1 ? highRange : "*")
                      + "/"
                      + (totalsize > -1 ? totalsize : "*"));

              // Content-Length refers to the current chunk size here, though in chunked
              // mode if the request is open-ended and totalsize is unknown we omit it.
              if (chunked && requested < 0 && totalsize < 0) {
                CLoverride = -1;
              } else {
                CLoverride = bytes;
              }
            } else {
              // Content-Length refers to the total remaining size of the stream here.
              CLoverride = remaining;
            }

            // Calculate the corresponding highRange (this is usually redundant).
            highRange = lowRange + CLoverride - (CLoverride > 0 ? 1 : 0);

            if (contentFeatures != null) {
              output.setHeader("ContentFeatures.DLNA.ORG", dlna.getDlnaContentFeatures());
            }

            output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
            output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
          }
        }
      }
    } else if ((method.equals("GET") || method.equals("HEAD"))
        && (argument.toLowerCase().endsWith(".png")
            || argument.toLowerCase().endsWith(".jpg")
            || argument.toLowerCase().endsWith(".jpeg"))) {
      if (argument.toLowerCase().endsWith(".png")) {
        output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/png");
      } else {
        output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/jpeg");
      }
      output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
      output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
      output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT");
      inputStream = getResourceInputStream(argument);
    } else if ((method.equals("GET") || method.equals("HEAD"))
        && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) {
      output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
      output.setHeader(HttpHeaders.Names.CACHE_CONTROL, "no-cache");
      output.setHeader(HttpHeaders.Names.EXPIRES, "0");
      output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes");
      output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
      inputStream =
          getResourceInputStream((argument.equals("description/fetch") ? "PMS.xml" : argument));
      if (argument.equals("description/fetch")) {
        byte b[] = new byte[inputStream.available()];
        inputStream.read(b);
        String s = new String(b);
        s = s.replace("[uuid]", PMS.get().usn()); // .substring(0, PMS.get().usn().length()-2));
        String profileName = PMS.getConfiguration().getProfileName();
        if (PMS.get().getServer().getHost() != null) {
          s = s.replace("[host]", PMS.get().getServer().getHost());
          s = s.replace("[port]", "" + PMS.get().getServer().getPort());
        }
        if (xbox) {
          LOGGER.debug("DLNA changes for Xbox 360");
          s =
              s.replace(
                  "Universal Media Server",
                  "Universal Media Server [" + profileName + "] : Windows Media Connect");
          s =
              s.replace(
                  "<modelName>UMS</modelName>", "<modelName>Windows Media Connect</modelName>");
          s =
              s.replace(
                  "<serviceList>",
                  "<serviceList>"
                      + CRLF
                      + "<service>"
                      + CRLF
                      + "<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>"
                      + CRLF
                      + "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>"
                      + CRLF
                      + "<SCPDURL>/upnp/mrr/scpd</SCPDURL>"
                      + CRLF
                      + "<controlURL>/upnp/mrr/control</controlURL>"
                      + CRLF
                      + "</service>"
                      + CRLF);
        } else {
          s = s.replace("Universal Media Server", "Universal Media Server [" + profileName + "]");
        }

        if (!mediaRenderer.isPS3()) {
          // hacky stuff. replace the png icon by a jpeg one. Like mpeg2 remux,
          // really need a proper format compatibility list by renderer
          s = s.replace("<mimetype>image/png</mimetype>", "<mimetype>image/jpeg</mimetype>");
          s = s.replace("/images/thumbnail-256.png", "/images/thumbnail-120.jpg");
          s = s.replace(">256<", ">120<");
        }
        response.append(s);
        inputStream = null;
      }
    } else if (method.equals("POST")
        && (argument.contains("MS_MediaReceiverRegistrar_control")
            || argument.contains("mrr/control"))) {
      output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
      response.append(HTTPXMLHelper.XML_HEADER);
      response.append(CRLF);
      response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
      response.append(CRLF);
      if (soapaction != null && soapaction.contains("IsAuthorized")) {
        response.append(HTTPXMLHelper.XBOX_2);
        response.append(CRLF);
      } else if (soapaction != null && soapaction.contains("IsValidated")) {
        response.append(HTTPXMLHelper.XBOX_1);
        response.append(CRLF);
      }
      response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER);
      response.append(CRLF);
      response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
      response.append(CRLF);
    } else if (method.equals("POST") && argument.endsWith("upnp/control/connection_manager")) {
      output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
      if (soapaction.indexOf("ConnectionManager:1#GetProtocolInfo") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.PROTOCOLINFO_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      }
    } else if (method.equals("POST") && argument.endsWith("upnp/control/content_directory")) {
      output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\"");
      if (soapaction.indexOf("ContentDirectory:1#GetSystemUpdateID") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_HEADER);
        response.append(CRLF);
        response.append("<Id>" + DLNAResource.getSystemUpdateId() + "</Id>");
        response.append(CRLF);
        response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction.indexOf("ContentDirectory:1#X_GetFeatureList")
          > -1) { // Added for Samsung 2012 TVs
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SAMSUNG_ERROR_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction.indexOf("ContentDirectory:1#GetSortCapabilities") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SORTCAPS_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction.indexOf("ContentDirectory:1#GetSearchCapabilities") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SEARCHCAPS_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction.contains("ContentDirectory:1#Browse")
          || soapaction.contains("ContentDirectory:1#Search")) {
        objectID = getEnclosingValue(content, "<ObjectID>", "</ObjectID>");
        String containerID = null;
        if ((objectID == null || objectID.length() == 0) && xbox) {
          containerID = getEnclosingValue(content, "<ContainerID>", "</ContainerID>");
          if (!containerID.contains("$")) {
            objectID = "0";
          } else {
            objectID = containerID;
            containerID = null;
          }
        }
        Object sI = getEnclosingValue(content, "<StartingIndex>", "</StartingIndex>");
        Object rC = getEnclosingValue(content, "<RequestedCount>", "</RequestedCount>");
        browseFlag = getEnclosingValue(content, "<BrowseFlag>", "</BrowseFlag>");

        if (sI != null) {
          startingIndex = Integer.parseInt(sI.toString());
        }

        if (rC != null) {
          requestCount = Integer.parseInt(rC.toString());
        }

        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);

        if (soapaction.contains("ContentDirectory:1#Search")) {
          response.append(HTTPXMLHelper.SEARCHRESPONSE_HEADER);
        } else {
          response.append(HTTPXMLHelper.BROWSERESPONSE_HEADER);
        }

        response.append(CRLF);
        response.append(HTTPXMLHelper.RESULT_HEADER);
        response.append(HTTPXMLHelper.DIDL_HEADER);

        if (soapaction.contains("ContentDirectory:1#Search")) {
          browseFlag = "BrowseDirectChildren";
        }

        // XBOX virtual containers ... d'oh!
        String searchCriteria = null;
        if (xbox
            && PMS.getConfiguration().getUseCache()
            && PMS.get().getLibrary() != null
            && containerID != null) {
          if (containerID.equals("7") && PMS.get().getLibrary().getAlbumFolder() != null) {
            objectID = PMS.get().getLibrary().getAlbumFolder().getResourceId();
          } else if (containerID.equals("6") && PMS.get().getLibrary().getArtistFolder() != null) {
            objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
          } else if (containerID.equals("5") && PMS.get().getLibrary().getGenreFolder() != null) {
            objectID = PMS.get().getLibrary().getGenreFolder().getResourceId();
          } else if (containerID.equals("F")
              && PMS.get().getLibrary().getPlaylistFolder() != null) {
            objectID = PMS.get().getLibrary().getPlaylistFolder().getResourceId();
          } else if (containerID.equals("4") && PMS.get().getLibrary().getAllFolder() != null) {
            objectID = PMS.get().getLibrary().getAllFolder().getResourceId();
          } else if (containerID.equals("1")) {
            String artist = getEnclosingValue(content, "upnp:artist = &quot;", "&quot;)");
            if (artist != null) {
              objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
              searchCriteria = artist;
            }
          }
        }

        List<DLNAResource> files =
            PMS.get()
                .getRootFolder(mediaRenderer)
                .getDLNAResources(
                    objectID,
                    browseFlag != null && browseFlag.equals("BrowseDirectChildren"),
                    startingIndex,
                    requestCount,
                    mediaRenderer);
        if (searchCriteria != null && files != null) {
          for (int i = files.size() - 1; i >= 0; i--) {
            if (!files.get(i).getName().equals(searchCriteria)) {
              files.remove(i);
            }
          }
          if (files.size() > 0) {
            files = files.get(0).getChildren();
          }
        }

        int minus = 0;
        if (files != null) {
          for (DLNAResource uf : files) {
            if (xbox && containerID != null) {
              uf.setFakeParentId(containerID);
            }
            if (uf.isCompatible(mediaRenderer)
                && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer))) {
              response.append(uf.toString(mediaRenderer));
            } else {
              minus++;
            }
          }
        }

        response.append(HTTPXMLHelper.DIDL_FOOTER);
        response.append(HTTPXMLHelper.RESULT_FOOTER);
        response.append(CRLF);
        int filessize = 0;
        if (files != null) {
          filessize = files.size();
        }
        response.append("<NumberReturned>").append(filessize - minus).append("</NumberReturned>");
        response.append(CRLF);
        DLNAResource parentFolder = null;
        if (files != null && filessize > 0) {
          parentFolder = files.get(0).getParent();
        }
        if (browseFlag != null
            && browseFlag.equals("BrowseDirectChildren")
            && mediaRenderer.isMediaParserV2()
            && mediaRenderer.isDLNATreeHack()) {
          // with the new parser, files are parsed and analyzed *before* creating the DLNA tree,
          // every 10 items (the ps3 asks 10 by 10),
          // so we do not know exactly the total number of items in the DLNA folder to send
          // (regular files, plus the #transcode folder, maybe the #imdb one, also files can be
          // invalidated and hidden if format is broken or encrypted, etc.).
          // let's send a fake total size to force the renderer to ask following items
          int totalCount = startingIndex + requestCount + 1; // returns 11 when 10 asked
          if (filessize - minus <= 0) // if no more elements, send the startingIndex
          {
            totalCount = startingIndex;
          }
          response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>");
        } else if (browseFlag != null && browseFlag.equals("BrowseDirectChildren")) {
          response
              .append("<TotalMatches>")
              .append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus)
              .append("</TotalMatches>");
        } else { // from upnp spec: If BrowseMetadata is specified in the BrowseFlags then
          // TotalMatches = 1
          response.append("<TotalMatches>1</TotalMatches>");
        }
        response.append(CRLF);
        response.append("<UpdateID>");
        if (parentFolder != null) {
          response.append(parentFolder.getUpdateId());
        } else {
          response.append("1");
        }
        response.append("</UpdateID>");
        response.append(CRLF);
        if (soapaction.contains("ContentDirectory:1#Search")) {
          response.append(HTTPXMLHelper.SEARCHRESPONSE_FOOTER);
        } else {
          response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER);
        }
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
        // LOGGER.trace(response.toString());
      }
    } else if (method.equals("SUBSCRIBE")) {
      output.setHeader("SID", PMS.get().usn());
      output.setHeader("TIMEOUT", "Second-1800");
      String cb = soapaction.replace("<", "").replace(">", "");
      String faddr = cb.replace("http://", "").replace("/", "");
      String addr = faddr.split(":")[0];
      int port = Integer.parseInt(faddr.split(":")[1]);
      Socket sock = new Socket(addr, port);
      OutputStream out = sock.getOutputStream();
      out.write(("NOTIFY /" + argument + " HTTP/1.1").getBytes());
      out.write(CRLF.getBytes());
      out.write(("SID: " + PMS.get().usn()).getBytes());
      out.write(CRLF.getBytes());
      out.write(("SEQ: " + 0).getBytes());
      out.write(CRLF.getBytes());
      out.write(("NT: upnp:event").getBytes());
      out.write(CRLF.getBytes());
      out.write(("NTS: upnp:propchange").getBytes());
      out.write(CRLF.getBytes());
      out.write(("HOST: " + faddr).getBytes());
      out.write(CRLF.getBytes());
      out.flush();
      out.close();
      if (argument.contains("connection_manager")) {
        response.append(
            HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ConnectionManager:1"));
        response.append(HTTPXMLHelper.eventProp("SinkProtocolInfo"));
        response.append(HTTPXMLHelper.eventProp("SourceProtocolInfo"));
        response.append(HTTPXMLHelper.eventProp("CurrentConnectionIDs"));
        response.append(HTTPXMLHelper.EVENT_FOOTER);
      } else if (argument.contains("content_directory")) {
        response.append(
            HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ContentDirectory:1"));
        response.append(HTTPXMLHelper.eventProp("TransferIDs"));
        response.append(HTTPXMLHelper.eventProp("ContainerUpdateIDs"));
        response.append(
            HTTPXMLHelper.eventProp("SystemUpdateID", "" + DLNAResource.getSystemUpdateId()));
        response.append(HTTPXMLHelper.EVENT_FOOTER);
      }
    } else if (method.equals("NOTIFY")) {
      output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml");
      output.setHeader("NT", "upnp:event");
      output.setHeader("NTS", "upnp:propchange");
      output.setHeader("SID", PMS.get().usn());
      output.setHeader("SEQ", "0");
      response.append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
      response.append("<e:property>");
      response.append("<TransferIDs></TransferIDs>");
      response.append("</e:property>");
      response.append("<e:property>");
      response.append("<ContainerUpdateIDs></ContainerUpdateIDs>");
      response.append("</e:property>");
      response.append("<e:property>");
      response
          .append("<SystemUpdateID>")
          .append(DLNAResource.getSystemUpdateId())
          .append("</SystemUpdateID>");
      response.append("</e:property>");
      response.append("</e:propertyset>");
    }

    output.setHeader("Server", PMS.get().getServerName());

    if (response.length() > 0) {
      // A response message was constructed; convert it to data ready to be sent.
      byte responseData[] = response.toString().getBytes("UTF-8");
      output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + responseData.length);

      // HEAD requests only require headers to be set, no need to set contents.
      if (!method.equals("HEAD")) {
        // Not a HEAD request, so set the contents of the response.
        ChannelBuffer buf = ChannelBuffers.copiedBuffer(responseData);
        output.setContent(buf);
      }

      // Send the response to the client.
      future = e.getChannel().write(output);

      if (close) {
        // Close the channel after the response is sent.
        future.addListener(ChannelFutureListener.CLOSE);
      }
    } else if (inputStream != null) {
      // There is an input stream to send as a response.

      if (CLoverride > -2) {
        // Content-Length override has been set, send or omit as appropriate
        if (CLoverride > -1 && CLoverride != DLNAMediaInfo.TRANS_SIZE) {
          // Since PS3 firmware 2.50, it is wiser not to send an arbitrary Content-Length,
          // as the PS3 will display a network error and request the last seconds of the
          // transcoded video. Better to send no Content-Length at all.
          output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + CLoverride);
        }
      } else {
        int cl = inputStream.available();
        LOGGER.trace("Available Content-Length: " + cl);
        output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + cl);
      }

      if (range.isStartOffsetAvailable() && dlna != null) {
        // Add timeseek information headers.
        String timeseekValue = DLNAMediaInfo.getDurationString(range.getStartOrZero());
        String timetotalValue = dlna.getMedia().getDurationString();
        String timeEndValue =
            range.isEndLimitAvailable()
                ? DLNAMediaInfo.getDurationString(range.getEnd())
                : timetotalValue;
        output.setHeader(
            "TimeSeekRange.dlna.org",
            "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue);
        output.setHeader(
            "X-Seek-Range", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue);
      }

      // Send the response headers to the client.
      future = e.getChannel().write(output);

      if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) {
        // Send the response body to the client in chunks.
        ChannelFuture chunkWriteFuture =
            e.getChannel().write(new ChunkedStream(inputStream, BUFFER_SIZE));

        // Add a listener to clean up after sending the entire response body.
        chunkWriteFuture.addListener(
            new ChannelFutureListener() {
              @Override
              public void operationComplete(ChannelFuture future) {
                try {
                  PMS.get().getRegistry().reenableGoToSleep();
                  inputStream.close();
                } catch (IOException e) {
                  LOGGER.debug("Caught exception", e);
                }

                // Always close the channel after the response is sent because of
                // a freeze at the end of video when the channel is not closed.
                future.getChannel().close();
                startStopListenerDelegate.stop();
              }
            });
      } else {
        // HEAD method is being used, so simply clean up after the response was sent.
        try {
          PMS.get().getRegistry().reenableGoToSleep();
          inputStream.close();
        } catch (IOException ioe) {
          LOGGER.debug("Caught exception", ioe);
        }

        if (close) {
          // Close the channel after the response is sent
          future.addListener(ChannelFutureListener.CLOSE);
        }

        startStopListenerDelegate.stop();
      }
    } else {
      // No response data and no input stream. Seems we are merely serving up headers.
      if (lowRange > 0 && highRange > 0) {
        // FIXME: There is no content, so why set a length?
        output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + (highRange - lowRange + 1));
      } else {
        output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0");
      }

      // Send the response headers to the client.
      future = e.getChannel().write(output);

      if (close) {
        // Close the channel after the response is sent.
        future.addListener(ChannelFutureListener.CLOSE);
      }
    }

    // Log trace information
    Iterator<String> it = output.getHeaderNames().iterator();

    while (it.hasNext()) {
      String headerName = it.next();
      LOGGER.trace("Sent to socket: " + headerName + ": " + output.getHeader(headerName));
    }

    return future;
  }
/**
 * Circular memory buffer that can be used as {@link java.io.OutputStream OutputStream} and provides
 * methods that can read data from the memory buffer using an {@link java.io.InputStream
 * InputStream}. The name of this class is a bit misleading, as there is typically no file involved
 * in the process at all. Instead, the buffer is typically used to hold data piped by a transcoding
 * process in one thread until a request for data comes in from another thread.
 *
 * @see net.pms.io.ProcessWrapperImpl
 * @see net.pms.network.Request Request
 * @see net.pms.network.RequestV2 RequestV2
 */
public class BufferedOutputFileImpl extends OutputStream implements BufferedOutputFile {
  private static final Logger LOGGER = LoggerFactory.getLogger(BufferedOutputFileImpl.class);
  private static final PmsConfiguration configuration = PMS.getConfiguration();

  /** Initial size for the buffer in bytes. The current value is 50MB. */
  private static final int INITIAL_BUFFER_SIZE = 50000000;

  /** Amount of extra bytes to increase the initial buffer with when memory allocation fails. */
  private static final int MARGIN_LARGE = 20000000;

  private static final int MARGIN_MEDIUM = 2000000;
  private static final int MARGIN_SMALL = 600000;
  private static final int CHECK_INTERVAL = 500;
  private static final int CHECK_END_OF_PROCESS = 2500; // must be superior to CHECK_INTERVAL
  private int minMemorySize;
  private int maxMemorySize;
  private int bufferOverflowWarning;
  private boolean eof;
  private long writeCount;
  private byte buffer[];
  private boolean forcefirst =
      (configuration.getTrancodeBlocksMultipleConnections()
          && configuration.getTrancodeKeepFirstConnections());
  private ArrayList<WaitBufferedInputStream> inputStreams;
  private ProcessWrapper attachedThread;
  private int secondread_minsize;
  private Timer timer;
  private boolean shiftScr;
  private FileOutputStream debugOutput = null;
  private boolean buffered = false;
  private DecimalFormat formatter = new DecimalFormat("#,###");
  private double timeseek;
  private double timeend;
  private long packetpos = 0;

  /**
   * Try to increase the size of a memory buffer, while retaining its contents. The provided new
   * size is considered to be a request, it is scaled down when an OutOfMemory error occurs. There
   * is no guarantee about the exact length of the returned byte array, only that it is greater than
   * or equal to the original buffer size. When null is passed as an argument, a fresh buffer will
   * be allocated. Copying one byte array to another is a costly operation, both in memory usage and
   * performance. It is best to avoid using this method.
   *
   * @param buffer The byte array to resize, null is allowed.
   * @param newSize The requested final size. Should be greater than the original size or the
   *     original buffer will be returned.
   * @return The resized byte array.
   */
  private byte[] growBuffer(byte[] buffer, int newSize) {
    byte[] copy;

    if (buffer == null) {
      // Temporary empty array to avoid null tests in the code below
      buffer = new byte[0];
    }

    if (newSize <= buffer.length) {
      // Cannot shrink the original
      return buffer;
    }

    try {
      // Try to allocate the requested new size
      copy = new byte[newSize];
    } catch (OutOfMemoryError e) {
      if (buffer.length == 0) {
        LOGGER.error("Cannot initialize buffer to " + formatter.format(newSize) + " bytes.", e);
      } else {
        LOGGER.warn(
            "Cannot grow buffer size from "
                + formatter.format(buffer.length)
                + " bytes to "
                + formatter.format(newSize)
                + " bytes.",
            e);
      }

      // Could not allocate the requested new size, use 30% of free memory instead.
      // Rationale behind using 30%: multiple threads are running at the same time,
      // we do not want one thread's memory usage to suffocate the others.
      // Using maxMemory() to ignore the initial Java heap space size that freeMemory()
      // takes into account.
      // See http://javarevisited.blogspot.com/2011/05/java-heap-space-memory-size-jvm.html
      long realisticSize = Runtime.getRuntime().maxMemory() * 3 / 10;

      if (realisticSize < buffer.length) {
        // A copy would be smaller in size, shrinking instead of growing the buffer.
        // Better to return the original and retain its size.
        return buffer;
      } else {
        try {
          // Try to allocate the realistic alternative size
          copy = new byte[(int) realisticSize];
        } catch (OutOfMemoryError e2) {
          LOGGER.error(
              "Cannot grow buffer size from "
                  + formatter.format(buffer.length)
                  + " bytes to "
                  + formatter.format(realisticSize)
                  + " bytes either.",
              e2);
          LOGGER.error("freeMemory: " + formatter.format(Runtime.getRuntime().freeMemory()));
          LOGGER.error("totalMemory: " + formatter.format(Runtime.getRuntime().totalMemory()));
          LOGGER.error("maxMemory: " + formatter.format(Runtime.getRuntime().maxMemory()));

          // Cannot allocate memory, no other option than to return the original.
          return buffer;
        }
      }
    }

    if (buffer.length == 0) {
      LOGGER.info(
          "Successfully initialized buffer to " + formatter.format(copy.length) + " bytes.");
    } else {
      try {
        System.arraycopy(buffer, 0, copy, 0, buffer.length);
        LOGGER.info(
            "Successfully grown buffer from "
                + formatter.format(buffer.length)
                + " bytes to "
                + formatter.format(copy.length)
                + " bytes.");
      } catch (Exception ex) {
        LOGGER.error("Cannot grow buffer size, error copying buffer contents.", ex);
      }
    }

    return copy;
  }

  /**
   * Constructor to create a memory buffer based on settings that are passed on. Will also start up
   * a timer task to display buffer size and usage in the PMS main screen.
   *
   * @param params {@link OutputParams} object that contains preferences for the buffers dimensions
   *     and behavior.
   */
  public BufferedOutputFileImpl(OutputParams params) {
    this.minMemorySize = (int) (1048576 * params.minBufferSize);
    this.maxMemorySize = (int) (1048576 * params.maxBufferSize);

    // FIXME: Better to relate margin directly to maxMemorySize instead of using arbitrary fixed
    // values

    int margin =
        MARGIN_LARGE; // Issue 220: extends to 20Mb : readCount is wrongly set cause of the ps3's
    // 2nd request with a range like 44-xxx, causing the end of buffer margin to be first sent
    if (this.maxMemorySize < margin) { // for thumbnails / small buffer usage
      margin =
          MARGIN_MEDIUM; // margin must be superior to the buffer size of OutputBufferConsumer or
                         // direct buffer size from WindowsNamedPipe class
      if (this.maxMemorySize < margin) {
        margin = MARGIN_SMALL;
      }
    }
    this.bufferOverflowWarning = this.maxMemorySize - margin;
    this.secondread_minsize = params.secondread_minsize;
    this.timeseek = params.timeseek;
    this.timeend = params.timeend;
    this.shiftScr = params.shift_scr;

    if ((maxMemorySize > INITIAL_BUFFER_SIZE) && !configuration.initBufferMax()) {
      // Try to limit memory usage a bit.
      // Start with a modest allocation initially, grow to max when needed later.
      buffer = growBuffer(null, INITIAL_BUFFER_SIZE);
    } else {
      buffer = growBuffer(null, maxMemorySize);
    }

    if (buffer.length == 0) {
      // Cannot transcode without a buffer
      LOGGER.info("FATAL ERROR: OutOfMemory / dumping stats");
      LOGGER.trace("freeMemory: " + Runtime.getRuntime().freeMemory());
      LOGGER.trace("totalMemory: " + Runtime.getRuntime().totalMemory());
      LOGGER.trace("maxMemory: " + Runtime.getRuntime().maxMemory());
      System.exit(1);
    }

    inputStreams = new ArrayList<WaitBufferedInputStream>();
    timer = new Timer();

    if (params.maxBufferSize > 15 && !params.hidebuffer) {
      timer.schedule(
          new TimerTask() {
            @Override
            public void run() {
              long rc = 0;

              if (getCurrentInputStream() != null) {
                rc = getCurrentInputStream().getReadCount();
                PMS.get().getFrame().setReadValue(rc, "");
              }

              long space = (writeCount - rc);
              LOGGER.trace(
                  "buffered: "
                      + formatter.format(space)
                      + " bytes / inputs: "
                      + inputStreams.size());

              // There are 1048576 bytes in a megabyte
              long bufferInMBs = space / 1048576;

              PMS.get()
                  .getFrame()
                  .setValue(
                      (int) (100 * space / maxMemorySize),
                      formatter.format(bufferInMBs) + " " + Messages.getString("StatusTab.12"));
            }
          },
          0,
          2000);
    }
  }

  @Override
  public void close() throws IOException {
    LOGGER.trace("EOF");
    eof = true;
  }

  @Override
  public WaitBufferedInputStream getCurrentInputStream() {
    WaitBufferedInputStream wai = null;

    if (inputStreams.size() > 0) {
      try {
        wai = forcefirst ? inputStreams.get(0) : inputStreams.get(inputStreams.size() - 1);
      } catch (IndexOutOfBoundsException e) {
        // this should never happen unless there's a concurrency issue,
        // so log it if it does
        LOGGER.error("Unexpected input stream removal", e);
      }
    }

    return wai;
  }

  @Override
  public InputStream getInputStream(long newReadPosition) {
    if (attachedThread != null) {
      attachedThread.setReadyToStop(false);
    }

    WaitBufferedInputStream atominputStream;

    if (!configuration.getTrancodeBlocksMultipleConnections() || getCurrentInputStream() == null) {
      atominputStream = new WaitBufferedInputStream(this);
      inputStreams.add(atominputStream);
    } else {
      if (configuration.getTrancodeKeepFirstConnections()) {
        LOGGER.debug(
            "BufferedOutputFile is already attached to an InputStream: " + getCurrentInputStream());
      } else {
        // Ditlew - fixes the above (the above iterator breaks on items getting close, cause they
        // will remove them self from the arraylist)
        while (inputStreams.size() > 0) {
          try {
            inputStreams.get(0).close();
          } catch (IOException e) {
            LOGGER.error("Error: ", e);
          }
        }

        inputStreams.clear();
        atominputStream = new WaitBufferedInputStream(this);
        inputStreams.add(atominputStream);
        LOGGER.debug("Reassign inputstream: " + getCurrentInputStream());
      }

      return null;
    }

    if (newReadPosition > 0) {
      LOGGER.debug("Setting InputStream new position to: " + formatter.format(newReadPosition));
      atominputStream.setReadCount(newReadPosition);
    }

    return atominputStream;
  }

  @Override
  public long getWriteCount() {
    return writeCount;
  }

  @Override
  public void write(byte b[], int off, int len) throws IOException {
    if (debugOutput != null) {
      debugOutput.write(b, off, len);
      debugOutput.flush();
    }

    WaitBufferedInputStream input = getCurrentInputStream();

    // LOGGER.trace("write(" + b.length + ", " + off + ", " + len + "), writeCount = " + writeCount
    // + ", readCount = " + (input != null ? input.getReadCount() : "null"));

    while ((input != null && (writeCount - input.getReadCount() > bufferOverflowWarning))
        || (input == null && writeCount > bufferOverflowWarning)) {
      try {
        Thread.sleep(CHECK_INTERVAL);
      } catch (InterruptedException e) {
      }
      input = getCurrentInputStream();
    }

    if (buffer != null) {
      int mb = (int) (writeCount % maxMemorySize);

      if (mb >= buffer.length - (len - off)) {
        if (buffer.length == INITIAL_BUFFER_SIZE) {
          // Initial buffer size was not big enough, try to increase it
          buffer = growBuffer(buffer, maxMemorySize);
        }

        // FIXME: This smells like 2x System.arraycopy()!
        int s = (len - off);
        for (int i = 0; i < s; i++) {
          buffer[modulo(mb + i, buffer.length)] = b[off + i];
        }
      } else {
        System.arraycopy(b, off, buffer, mb, (len - off));
        if ((len - off) > 0) {
          buffered = true;
        }
      }

      // Ditlew - WDTV Live
      if (timeseek > 0 && writeCount > 10) {
        for (int i = 0; i < len; i++) {
          if (buffer != null && shiftScr) {
            shiftSCRByTimeSeek(mb + i, (int) timeseek); // Ditlew - update any SCR headers
          } // shiftGOPByTimeSeek(mb+i, (int)timeseek); // Ditlew - update any GOP headers - Not
            // needed for WDTV Live
        }
      }

      writeCount += len - off;
      if (timeseek > 0 && timeend == 0) {
        int packetLength = 6; // minimum to get packet size
        while (packetpos + packetLength < writeCount && buffer != null) {
          int packetposMB = (int) (packetpos % maxMemorySize);
          int streamPos = 0;
          if (buffer[modulo(packetposMB, buffer.length)] == 71) { // TS
            packetLength = 188;
            streamPos = 4;

            // adaptation field
            if ((buffer[modulo(packetposMB + 3, buffer.length)] & 0x20) == 0x20) {
              streamPos += 1 + ((buffer[modulo(packetposMB + 4, buffer.length)] + 256) % 256);
            }

            if (streamPos == 188) {
              streamPos = -1;
            }

          } else if (buffer[modulo(packetposMB + 3, buffer.length)] == -70) { // BA
            packetLength = 14;
            streamPos = -1;
          } else {
            packetLength =
                6
                    + (((buffer[modulo(packetposMB + 4, buffer.length)] + 256) % 256)) * 256
                    + ((buffer[modulo(packetposMB + 5, buffer.length)] + 256) % 256);
          }
          if (streamPos != -1) {
            mb = packetposMB + streamPos + 18;
            if (!shiftVideo(mb, true)) {
              mb = mb - 5;
              shiftAudio(mb, true);
            }
          }
          packetpos += packetLength;
        }
      }
    }
  }

  /**
   * Determine a modulo value that is guaranteed to be zero or positive, as opposed to the standard
   * Java % operator which can return a negative value.
   *
   * @param number Number to divide
   * @param divisor Number that is used to divide
   * @return The rest value of the division.
   */
  private int modulo(int number, int divisor) {
    if (number >= 0) {
      return number % divisor;
    }
    return ((number % divisor) + divisor) % divisor;
  }

  @Override
  public void write(int b) throws IOException {
    boolean bb = b % 100000 == 0;
    WaitBufferedInputStream input = getCurrentInputStream();
    while (bb
        && ((input != null && (writeCount - input.getReadCount() > bufferOverflowWarning))
            || (input == null && writeCount == bufferOverflowWarning))) {
      try {
        Thread.sleep(CHECK_INTERVAL);
        // LOGGER.trace("BufferedOutputFile Full");
      } catch (InterruptedException e) {
      }
      input = getCurrentInputStream();
    }
    int mb = (int) (writeCount++ % maxMemorySize);
    if (buffer != null) {
      buffer[mb] = (byte) b;
      buffered = true;
      if (writeCount == INITIAL_BUFFER_SIZE) {
        buffer = growBuffer(buffer, maxMemorySize);
      }

      if (timeseek > 0 && writeCount > 19) {
        shiftByTimeSeek(mb, mb <= 20);
      }

      // Ditlew - WDTV Live - update any SCR headers
      if (timeseek > 0 && writeCount > 10) {
        shiftSCRByTimeSeek(mb, (int) timeseek);
      }
    }
  }

  // Ditlew - Modify SCR
  private void shiftSCRByTimeSeek(int buffer_index, int offset_sec) {
    int m9 = modulo(buffer_index - 9, buffer.length);
    int m8 = modulo(buffer_index - 8, buffer.length);
    int m7 = modulo(buffer_index - 7, buffer.length);
    int m6 = modulo(buffer_index - 6, buffer.length);
    int m5 = modulo(buffer_index - 5, buffer.length);
    int m4 = modulo(buffer_index - 4, buffer.length);
    int m3 = modulo(buffer_index - 3, buffer.length);
    int m2 = modulo(buffer_index - 2, buffer.length);
    int m1 = modulo(buffer_index - 1, buffer.length);
    int m0 = modulo(buffer_index, buffer.length);

    // SCR
    if (buffer[m9] == 0
        && buffer[m8] == 0
        && buffer[m7] == 1
        && buffer[m6] == -70
        && // 0xBA - Java/PMS wants -70
        // control bits
        !((buffer[m5] & 128) == 128)
        && ((buffer[m5] & 64) == 64)
        && ((buffer[m5] & 4) == 4)
        && ((buffer[m3] & 4) == 4)
        && ((buffer[m1] & 4) == 4)
        && ((buffer[m0] & 1) == 1)) {
      long scr_32_30 = ((buffer[m5] & 56) >> 3);
      long scr_29_15 = ((buffer[m5] & 3) << 13) + (buffer[m4] << 5) + ((buffer[m3] & 248) >> 3);
      long scr_14_00 = ((buffer[m3] & 3) << 13) + (buffer[m2] << 5) + ((buffer[m1] & 248) >> 3);

      long scr = (scr_32_30 << 30) + (scr_29_15 << 15) + scr_14_00;
      long scr_new = scr + (90000L * offset_sec);

      long scr_32_30_new = (scr_new & 7516192768L) >> 30; // 111000000000000000000000000000000
      long scr_29_15_new = (scr_new & 1073709056L) >> 15; // 000111111111111111000000000000000
      long scr_14_00_new = (scr_new & 32767L); // 000000000000000000111111111111111

      // scr_32_30_new
      buffer[m5] = (byte) ((buffer[m5] & 199) + ((scr_32_30_new << 3) & 56)); // 11000111

      // scr_29_15_new
      buffer[m5] = (byte) ((buffer[m5] & 252) + ((scr_29_15_new >> 13) & 3)); // 00000011
      buffer[m4] = (byte) (scr_29_15_new >> 5); // 11111111
      buffer[m3] = (byte) ((buffer[m3] & 7) + ((scr_29_15_new << 3) & 248)); // 11111000

      // scr_14_00_new
      buffer[m3] = (byte) ((buffer[m3] & 252) + ((scr_14_00_new >> 13) & 3)); // 00000011
      buffer[m2] = (byte) (scr_14_00_new >> 5); // 11111111
      buffer[m1] = (byte) ((buffer[m1] & 7) + ((scr_14_00_new << 3) & 248)); // 11111000

      // Debug
      // LOGGER.trace("Ditlew - SCR "+scr+" ("+(int)(scr/90000)+") -> "+scr_new+"
      // ("+(int)(scr_new/90000)+")  "+offset_sec+" secs");
    }
  }

  // Ditlew - Modify GOP
  @SuppressWarnings("unused")
  private void shiftGOPByTimeSeek(int buffer_index, int offset_sec) {
    int m7 = modulo(buffer_index - 7, buffer.length);
    int m6 = modulo(buffer_index - 6, buffer.length);
    int m5 = modulo(buffer_index - 5, buffer.length);
    int m4 = modulo(buffer_index - 4, buffer.length);
    int m3 = modulo(buffer_index - 3, buffer.length);
    int m2 = modulo(buffer_index - 2, buffer.length);
    int m1 = modulo(buffer_index - 1, buffer.length);
    int m0 = modulo(buffer_index, buffer.length);

    // check if valid gop
    if (buffer[m7] == 0
        && buffer[m6] == 0
        && buffer[m5] == 1
        && buffer[m4] == -72
        && // 0xB8 - Java/PMS wants -72
        // control bits
        ((buffer[m2] & 0x08) == 0x08)
        && ((buffer[m0] & 31) == 0)
        && // of interest
        !((buffer[m3] & 128) == 128)
        && // not drop frm
        !((buffer[m0] & 16) == 16) // not broken
    ) {
      // org timecode
      byte h = (byte) ((buffer[m3] & 124) >> 2);
      byte m = (byte) (((buffer[m3] & 3) << 4) + ((buffer[m2] & 240) >> 4));
      byte s = (byte) (((buffer[m2] & 7) << 3) + ((buffer[m1] & 224) >> 5));

      // updated offset
      int _offset = s + m * 60 + h * 60 + offset_sec;

      // new timecode
      byte _h = (byte) ((_offset / 3600) % 24);
      byte _m = (byte) ((_offset / 60) % 60);
      byte _s = (byte) (_offset % 60);

      // update gop
      // h - ok
      buffer[m3] = (byte) ((buffer[m3] & 131) + (_h << 2)); // 10000011
      // m - ok
      buffer[m3] = (byte) ((buffer[m3] & 252) + (_m >> 4)); // 11111100
      buffer[m2] = (byte) ((buffer[m2] & 15) + (_m << 4)); // 00001111
      // s - ok
      buffer[m2] = (byte) ((buffer[m2] & 248) + (_s >> 3)); // 11111000
      buffer[m1] = (byte) ((buffer[m1] & 31) + (_s << 5)); // 00011111

      // Debug
      // LOGGER.trace("Ditlew - GOP "+h+":"+m+":"+s+" -> "+_h+":"+_m+":"+_s+"  "+offset_sec+"
      // secs");
    }
  }

  private void shiftByTimeSeek(int mb, boolean mod) {
    shiftVideo(mb, mod);
    shiftAudio(mb, mod);
  }

  private boolean shiftAudio(int mb, boolean mod) {
    boolean bb =
        (!mod
                && (buffer[mb - 10] == -67 || buffer[mb - 10] == -64)
                && buffer[mb - 11] == 1
                && buffer[mb - 12] == 0
                && buffer[mb - 13] == 0
                && /*(buffer[mb-7]&128)==128 &&*/ (buffer[mb - 6] & 128)
                    == 128 /*buffer[mb-6] == -128*/)
            || (mod
                && (buffer[modulo(mb - 10, buffer.length)] == -67
                    || buffer[modulo(mb - 10, buffer.length)] == -64)
                && buffer[modulo(mb - 11, buffer.length)] == 1
                && buffer[modulo(mb - 12, buffer.length)] == 0
                && buffer[modulo(mb - 13, buffer.length)] == 0
                && /*(buffer[modulo(mb-7)]&128)==128 && */ (buffer[modulo(mb - 6, buffer.length)]
                        & 128)
                    == 128 /*buffer[modulo(mb-6, buffer.length)] == -128*/);
    if (bb) {
      int pts =
          (((((buffer[modulo(mb - 3, buffer.length)] & 0xff) << 8)
                          + (buffer[modulo(mb - 2, buffer.length)] & 0xff))
                      >> 1)
                  << 15)
              + ((((buffer[modulo(mb - 1, buffer.length)] & 0xff) << 8)
                      + (buffer[modulo(mb, buffer.length)] & 0xff))
                  >> 1);
      pts += (int) (timeseek * 90000);

      setTS(pts, mb, mod);
      return true;
    }
    return false;
  }

  private boolean shiftVideo(int mb, boolean mod) {
    boolean bb =
        (!mod
                && (buffer[mb - 15] == -32 || buffer[mb - 15] == -3)
                && buffer[mb - 16] == 1
                && buffer[mb - 17] == 0
                && buffer[mb - 18] == 0
                && (buffer[mb - 11] & 128) == 128
                && (buffer[mb - 9] & 32) == 32)
            || (mod
                && (buffer[modulo(mb - 15, buffer.length)] == -32
                    || buffer[modulo(mb - 15, buffer.length)] == -3)
                && buffer[modulo(mb - 16, buffer.length)] == 1
                && buffer[modulo(mb - 17, buffer.length)] == 0
                && buffer[modulo(mb - 18, buffer.length)] == 0
                && (buffer[modulo(mb - 11, buffer.length)] & 128) == 128
                && (buffer[modulo(mb - 9, buffer.length)] & 32) == 32);

    if (bb) { // check EO or FD (tsMuxeR)
      int pts = getTS(mb - 5, mod);
      int dts = 0;
      boolean dts_present = (buffer[modulo(mb - 11, buffer.length)] & 64) == 64;
      if (dts_present) {
        if ((buffer[modulo(mb - 4, buffer.length)] & 15) == 15) {
          dts =
              (((((255 - (buffer[modulo(mb - 3, buffer.length)] & 0xff)) << 8)
                              + (255 - (buffer[modulo(mb - 2, buffer.length)] & 0xff)))
                          >> 1)
                      << 15)
                  + ((((255 - (buffer[modulo(mb - 1, buffer.length)] & 0xff)) << 8)
                          + (255 - (buffer[modulo(mb, buffer.length)] & 0xff)))
                      >> 1);
          dts = -dts;
        } else {
          dts = getTS(mb, mod);
        }
      }

      int ts = (int) (timeseek * 90000);
      if (mb == 50 && writeCount < maxMemorySize) {
        dts--;
      }
      pts += ts;

      setTS(pts, mb - 5, mod);
      if (dts_present) {
        if (dts < 0) {
          buffer[modulo(mb - 4, buffer.length)] = 17;
        }
        dts += ts;
        setTS(dts, mb, mod);
      }
      return true;
    }
    return false;
  }

  private int getTS(int mb, boolean modulo) {
    int m3 = mb - 3;
    int m2 = mb - 2;
    int m1 = mb - 1;
    int m0 = mb;
    if (modulo) {
      m3 = modulo(m3, buffer.length);
      m2 = modulo(m2, buffer.length);
      m1 = modulo(m1, buffer.length);
      m0 = modulo(m0, buffer.length);
    }

    return (((((buffer[m3] & 0xff) << 8) + (buffer[m2] & 0xff)) >> 1) << 15)
        + ((((buffer[m1] & 0xff) << 8) + (buffer[m0] & 0xff)) >> 1);
  }

  private void setTS(int ts, int mb, boolean modulo) {
    int m3 = mb - 3;
    int m2 = mb - 2;
    int m1 = mb - 1;
    int m0 = mb;
    if (modulo) {
      m3 = modulo(m3, buffer.length);
      m2 = modulo(m2, buffer.length);
      m1 = modulo(m1, buffer.length);
      m0 = modulo(m0, buffer.length);
    }
    int pts_low = ts & 32767;
    int pts_high = (ts >> 15) & 32767;
    int pts_left_low = 1 + (pts_low << 1);
    int pts_left_high = 1 + (pts_high << 1);
    buffer[m3] = (byte) ((pts_left_high & 65280) >> 8);
    buffer[m2] = (byte) (pts_left_high & 255);
    buffer[m1] = (byte) ((pts_left_low & 65280) >> 8);
    buffer[m0] = (byte) (pts_left_low & 255);
  }

  @Override
  public int read(boolean firstRead, long readCount, byte buf[], int off, int len) {
    if (readCount > INITIAL_BUFFER_SIZE && readCount < maxMemorySize) {
      int newMargin = maxMemorySize - MARGIN_MEDIUM;
      if (bufferOverflowWarning != newMargin) {
        LOGGER.debug("Setting margin to 2Mb");
      }

      this.bufferOverflowWarning = newMargin;
    }

    if (eof && readCount >= writeCount) {
      return -1;
    }

    int c = 0;
    int minBufferS = firstRead ? minMemorySize : secondread_minsize;
    while (writeCount - readCount <= minBufferS && !eof && c < 15) {
      if (c == 0) {
        LOGGER.trace("Suspend Read: readCount=" + readCount + " / writeCount=" + writeCount);
      }

      c++;

      try {
        Thread.sleep(CHECK_INTERVAL);
      } catch (InterruptedException e) {
      }
    }

    if (attachedThread != null) {
      attachedThread.setReadyToStop(false);
    }

    if (c > 0) {
      LOGGER.trace("Resume Read: readCount=" + readCount + " / writeCount=" + writeCount);
    }

    if (buffer == null || !buffered) {
      return -1;
    }

    int mb = (int) (readCount % maxMemorySize);
    int endOF = buffer.length;
    int cut = 0;

    if (eof && (writeCount - readCount) < len) {
      cut = (int) (len - (writeCount - readCount));
    }

    if (mb >= endOF - len) {
      try {
        System.arraycopy(buffer, mb, buf, off, endOF - mb - cut);
      } catch (ArrayIndexOutOfBoundsException ex) {
        LOGGER.error("Something went wrong with the buffer.", ex);
        LOGGER.error("buffer.length: " + formatter.format(buffer.length) + " bytes.");
        LOGGER.error("mb: " + mb);
        LOGGER.error("buf.length: " + formatter.format(buf.length) + " bytes.");
        LOGGER.error("off: " + off);
        LOGGER.error("endOF - mb - cut: " + (endOF - mb - cut));
      }
      return endOF - mb;
    } else {
      System.arraycopy(buffer, mb, buf, off, len - cut);
      return len;
    }
  }

  @Override
  public int read(boolean firstRead, long readCount) {
    if (readCount > INITIAL_BUFFER_SIZE && readCount < maxMemorySize) {
      int newMargin = maxMemorySize - MARGIN_MEDIUM;

      if (bufferOverflowWarning != newMargin) {
        LOGGER.debug("Setting margin to 2Mb");
      }

      this.bufferOverflowWarning = newMargin;
    }

    if (eof && readCount >= writeCount) {
      return -1;
    }

    int c = 0;
    int minBufferS = firstRead ? minMemorySize : secondread_minsize;

    while (writeCount - readCount <= minBufferS && !eof && c < 15) {
      if (c == 0) {
        LOGGER.trace("Suspend Read: readCount=" + readCount + " / writeCount=" + writeCount);
      }

      c++;

      try {
        Thread.sleep(CHECK_INTERVAL);
      } catch (InterruptedException e) {
      }
    }

    if (attachedThread != null) {
      attachedThread.setReadyToStop(false);
    }

    if (c > 0) {
      LOGGER.trace("Resume Read: readCount=" + readCount + " / writeCount=" + writeCount);
    }

    if (buffer == null || !buffered) {
      return -1;
    }

    try {
      return 0xff & buffer[(int) (readCount % maxMemorySize)];
    } catch (ArrayIndexOutOfBoundsException ex) {
      LOGGER.error("Buffer read ArrayIndexOutOfBoundsException error.", ex);
      LOGGER.error("buffer.length: " + formatter.format(buffer.length) + " bytes.");
      LOGGER.error("readCount: \"" + readCount + "\"");
      LOGGER.error("maxMemorySize: \"" + maxMemorySize + "\"");
      return -1;
    }
  }

  @Override
  public void attachThread(ProcessWrapper thread) {
    if (attachedThread != null) {
      throw new RuntimeException(
          "BufferedOutputFile is already attached to a Thread: " + attachedThread);
    }

    LOGGER.debug("Attaching thread: " + thread);
    attachedThread = thread;
  }

  @Override
  public void removeInputStream(WaitBufferedInputStream inputStream) {
    inputStreams.remove(inputStream);
  }

  @Override
  public void detachInputStream() {
    PMS.get().getFrame().setReadValue(0, "");

    if (attachedThread != null) {
      attachedThread.setReadyToStop(true);
    }

    Runnable checkEnd =
        new Runnable() {
          @Override
          public void run() {
            try {
              Thread.sleep(CHECK_END_OF_PROCESS);
            } catch (InterruptedException e) {
              LOGGER.error(null, e);
            }

            if (attachedThread != null && attachedThread.isReadyToStop()) {
              if (!attachedThread.isDestroyed()) {
                attachedThread.stopProcess();
              }

              reset();
            }
          }
        };
    new Thread(checkEnd, "Buffered IO End Checker").start();
  }

  @Override
  public synchronized void reset() {
    if (debugOutput != null) {
      try {
        debugOutput.close();
      } catch (IOException ex) {
        LOGGER.warn("Caught exception", ex);
      }
    }

    timer.cancel();

    if (buffer != null) {
      LOGGER.info("Destroying buffer");
      buffer = null;
    }

    buffered = false;

    if (maxMemorySize != 1048576) {
      PMS.get().getFrame().setValue(0, Messages.getString("StatusTab.5"));
    }
  }
}
  /**
   * Returns the filename after being "prettified", which involves attempting to strip away certain
   * things like information about the quality, resolution, codecs, release groups, fansubbers,
   * etc., replacing periods with spaces, and various other things to produce a more "pretty" and
   * standardized filename.
   *
   * @param f The filename
   * @param file The file to possibly be used by the InfoDb
   * @return The prettified filename
   */
  public static String getFileNameWithRewriting(String f, File file) {
    String fileNameWithoutExtension;
    String formattedName;
    String formattedNameTemp;
    String searchFormattedName;

    // These are false unless we recognize that we could use some info on the video from IMDB
    boolean isEpisodeToLookup = false;
    boolean isMovieToLookup = false;

    // Remove file extension
    fileNameWithoutExtension = getFileNameWithoutExtension(f);
    formattedName = fileNameWithoutExtension;
    searchFormattedName = "";

    String commonFileEnds =
        "[\\s\\.]AC3.*|[\\s\\.]REPACK.*|[\\s\\.]480p.*|[\\s\\.]720p.*|[\\s\\.]m-720p.*|[\\s\\.]900p.*|[\\s\\.]1080p.*|[\\s\\.]HDTV.*|[\\s\\.]DSR.*|[\\s\\.]PDTV.*|[\\s\\.]WS.*|[\\s\\.]HQ.*|[\\s\\.]DVDRip.*|[\\s\\.]TVRiP.*|[\\s\\.]BDRip.*|[\\s\\.]WEBRip.*|[\\s\\.]BluRay.*|[\\s\\.]Blu-ray.*|[\\s\\.]SUBBED.*|[\\s\\.]x264.*|[\\s\\.]Dual[\\s\\.]Audio.*|[\\s\\.]HSBS.*|[\\s\\.]H-SBS.*|[\\s\\.]RERiP.*|[\\s\\.]DIRFIX.*|[\\s\\.]READNFO.*|[\\s\\.]60FPS.*";
    String commonFileEndsMatch =
        ".*[\\s\\.]AC3.*|.*[\\s\\.]REPACK.*|.*[\\s\\.]480p.*|.*[\\s\\.]720p.*|.*[\\s\\.]m-720p.*|.*[\\s\\.]900p.*|.*[\\s\\.]1080p.*|.*[\\s\\.]HDTV.*|.*[\\s\\.]DSR.*|.*[\\s\\.]PDTV.*|.*[\\s\\.]WS.*|.*[\\s\\.]HQ.*|.*[\\s\\.]DVDRip.*|.*[\\s\\.]TVRiP.*|.*[\\s\\.]BDRip.*|.*[\\s\\.]WEBRip.*|.*[\\s\\.]BluRay.*|.*[\\s\\.]Blu-ray.*|.*[\\s\\.]SUBBED.*|.*[\\s\\.]x264.*|.*[\\s\\.]Dual[\\s\\.]Audio.*|.*[\\s\\.]HSBS.*|.*[\\s\\.]H-SBS.*|.*[\\s\\.]RERiP.*|.*[\\s\\.]DIRFIX.*|.*[\\s\\.]READNFO.*|.*[\\s\\.]60FPS.*";
    String commonFileEndsCaseSensitive =
        "[\\s\\.]PROPER[\\s\\.].*|[\\s\\.]iNTERNAL[\\s\\.].*|[\\s\\.]LIMITED[\\s\\.].*|[\\s\\.]LiMiTED[\\s\\.].*|[\\s\\.]FESTiVAL[\\s\\.].*|[\\s\\.]NORDIC[\\s\\.].*|[\\s\\.]REAL[\\s\\.].*|[\\s\\.]SUBBED[\\s\\.].*|[\\s\\.]RETAIL[\\s\\.].*";

    String commonFileMiddle =
        "(?i)(Special[\\s\\.]Edition|Unrated|Final[\\s\\.]Cut|Remastered|Extended[\\s\\.]Cut|Extended|IMAX[\\s\\.]Edition)";

    if (formattedName.matches(".*[sS]0\\d[eE]\\d\\d[eE]\\d\\d.*")) {
      // This matches scene and most p2p TV episodes within the first 9 seasons that are double or
      // triple episodes

      // Rename the season/episode numbers. For example, "S01E01" changes to " - 101"
      // Then strip the end of the episode if it does not have the episode name in the title
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEnds + ")",
              " - $1$2$3-$1$4$5");
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")",
              " - $1$2$3-$1$4$5");

      // If it matches this then it didn't match the previous one, which means there is probably an
      // episode title in the filename
      formattedNameTemp =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3-$1$4$5 - ");

      if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) {
        isEpisodeToLookup = true;
      }

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");

      // Capitalize the first letter of each word if the string contains no capital letters
      if (formattedName.equals(formattedName.toLowerCase())) {
        formattedName = StringUtils.capitaliseAllWords(formattedName);
      }
    } else if (formattedName.matches(".*[sS][1-9]\\d[eE]\\d\\d[eE]\\d\\d.*")) {
      // This matches scene and most p2p TV episodes after their first 9 seasons that are double
      // episodes

      // Rename the season/episode numbers. For example, "S11E01" changes to " - 1101"
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEnds + ")",
              " - $1$2$3-$1$4$5");
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")",
              " - $1$2$3-$1$4$5");

      // If it matches this then it didn't match the previous one, which means there is probably an
      // episode title in the filename
      formattedNameTemp =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3-$1$4$5 - ");

      if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) {
        isEpisodeToLookup = true;
      }

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");

      // Capitalize the first letter of each word if the string contains no capital letters
      if (formattedName.equals(formattedName.toLowerCase())) {
        formattedName = StringUtils.capitaliseAllWords(formattedName);
      }
    } else if (formattedName.matches(".*[sS]0\\d[eE]\\d\\d.*")) {
      // This matches scene and most p2p TV episodes within the first 9 seasons

      // Rename the season/episode numbers. For example, "S01E01" changes to " - 101"
      // Then strip the end of the episode if it does not have the episode name in the title
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)(" + commonFileEnds + ")", " - $1$2$3");
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")", " - $1$2$3");

      // If it matches this then it didn't match the previous one, which means there is probably an
      // episode title in the filename
      formattedNameTemp =
          formattedName.replaceAll("(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3 - ");

      if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) {
        isEpisodeToLookup = true;
      }

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");

      // Capitalize the first letter of each word if the string contains no capital letters
      if (formattedName.equals(formattedName.toLowerCase())) {
        formattedName = StringUtils.capitaliseAllWords(formattedName);
      }
    } else if (formattedName.matches(".*[sS][1-9]\\d[eE]\\d\\d.*")) {
      // This matches scene and most p2p TV episodes after their first 9 seasons

      // Rename the season/episode numbers. For example, "S11E01" changes to " - 1101"
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)(" + commonFileEnds + ")", " - $1$2$3");
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")",
              " - $1$2$3");

      // If it matches this then it didn't match the previous one, which means there is probably an
      // episode title in the filename
      formattedNameTemp =
          formattedName.replaceAll("(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3 - ");

      if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) {
        isEpisodeToLookup = true;
      }

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");

      // Capitalize the first letter of each word if the string contains no capital letters
      if (formattedName.equals(formattedName.toLowerCase())) {
        formattedName = StringUtils.capitaliseAllWords(formattedName);
      }
    } else if (formattedName.matches(
        ".*[\\s\\.](19|20)\\d\\d[\\s\\.][0-1]\\d[\\s\\.][0-3]\\d[\\s\\.].*")) {
      // This matches scene and most p2p TV episodes that release several times per week

      // Rename the date. For example, "2013.03.18" changes to " - 2013/03/18"
      formattedName =
          formattedName.replaceAll(
              "(?i)[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)("
                  + commonFileEnds
                  + ")",
              " - $1$2/$3/$4");

      // If it matches this then it didn't match the previous one, which means there is probably an
      // episode title in the filename
      formattedNameTemp =
          formattedName.replaceAll(
              "(?i)[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)[\\s\\.]",
              " - $1$2/$3/$4 - ");

      if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) {
        isEpisodeToLookup = true;
      }

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");

      // Capitalize the first letter of each word if the string contains no capital letters
      if (formattedName.equals(formattedName.toLowerCase())) {
        formattedName = StringUtils.capitaliseAllWords(formattedName);
      }
    } else if (formattedName.matches(".*[\\s\\.](19|20)\\d\\d[\\s\\.].*")) {
      // This matches scene and most p2p movies

      // Rename the year. For example, "2013" changes to " (2013)"
      formattedName = formattedName.replaceAll("[\\s\\.](19|20)(\\d\\d)", " ($1$2)");

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");

      formattedName = formattedName.replaceAll(commonFileMiddle, "($1)");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");
    } else if (formattedName.matches(".*\\[(19|20)\\d\\d\\].*")) {
      // This matches rarer types of movies

      // Rename the year. For example, "2013" changes to " (2013)"
      formattedName = formattedName.replaceAll("(?i)\\[(19|20)(\\d\\d)\\].*", " ($1$2)");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");
    } else if (formattedName.matches(".*\\((19|20)\\d\\d\\).*")) {
      // This matches rarer types of movies

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");
    } else if (formattedName.matches(".*\\((19|20)\\d\\d\\).*")) {
      // This matches rarer types of movies

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");
    } else if (formattedName.matches(commonFileEndsMatch)) {
      // This is probably a movie that doesn't specify a year
      isMovieToLookup = true;

      // Remove stuff at the end of the filename like release group, quality, source, etc.
      formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, "");
      formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, "");
      formattedName = formattedName.replaceAll(commonFileMiddle, "($1)");

      // Replace periods with spaces
      formattedName = formattedName.replaceAll("\\.", " ");
    } else if (formattedName.matches(".*\\[[0-9a-zA-Z]{8}\\]$")) {
      // This matches anime with a hash at the end of the name

      // Remove underscores
      formattedName = formattedName.replaceAll("_", " ");

      // Remove stuff at the end of the filename like hash, quality, source, etc.
      formattedName =
          formattedName.replaceAll(
              "(?i)\\s\\(1280x720.*|\\s\\(1920x1080.*|\\s\\(720x400.*|\\[720p.*|\\[1080p.*|\\[480p.*|\\s\\(BD.*|\\s\\[Blu-Ray.*|\\s\\[DVD.*|\\.DVD.*|\\[[0-9a-zA-Z]{8}\\]$|\\[h264.*|R1DVD.*|\\[BD.*",
              "");

      // Remove group name from the beginning of the filename
      if (!"".equals(formattedName)) {
        if (formattedName.substring(0, 1).matches("\\[")) {
          int closingBracketIndex = formattedName.indexOf(']');
          if (closingBracketIndex != -1) {
            formattedName = formattedName.substring(closingBracketIndex + 1);
          }

          if (formattedName.substring(0, 1).matches("\\s")) {
            formattedName = formattedName.substring(1);
          }
        }
      } else {
        formattedName = fileNameWithoutExtension;
      }

      if (PMS.getConfiguration().isUseInfoFromIMDB()
          && formattedName.substring(formattedName.length() - 3).matches("[\\s\\._]\\d\\d")) {
        isEpisodeToLookup = true;
        searchFormattedName =
            formattedName.substring(0, formattedName.length() - 2)
                + "S01E"
                + formattedName.substring(formattedName.length() - 2);
      }
    } else if (formattedName.matches(
        ".*\\[BD\\].*|.*\\[720p\\].*|.*\\[1080p\\].*|.*\\[480p\\].*|.*\\[Blu-Ray.*|.*\\[h264.*")) {
      // This matches anime without a hash in the name

      // Remove underscores
      formattedName = formattedName.replaceAll("_", " ");

      // Remove stuff at the end of the filename like hash, quality, source, etc.
      formattedName =
          formattedName.replaceAll(
              "(?i)\\[BD\\].*|\\[720p.*|\\[1080p.*|\\[480p.*|\\[Blu-Ray.*\\[h264.*", "");

      // Remove group name from the beginning of the filename
      if (!"".equals(formattedName)) {
        if (formattedName.substring(0, 1).matches("\\[")) {
          int closingBracketIndex = formattedName.indexOf(']');
          if (closingBracketIndex != -1) {
            formattedName = formattedName.substring(closingBracketIndex + 1);
          }

          if (formattedName.substring(0, 1).matches("\\s")) {
            formattedName = formattedName.substring(1);
          }
        }
      } else {
        formattedName = fileNameWithoutExtension;
      }

      if (PMS.getConfiguration().isUseInfoFromIMDB()
          && formattedName.substring(formattedName.length() - 3).matches("[\\s\\._]\\d\\d")) {
        isEpisodeToLookup = true;
        searchFormattedName =
            formattedName.substring(0, formattedName.length() - 2)
                + "S01E"
                + formattedName.substring(formattedName.length() - 2);
      }
    }

    // Add episode name (if not there)
    if (isEpisodeToLookup || isMovieToLookup) {
      InfoDb.InfoDbData info = PMS.get().infoDb().get(file);
      if (info == null) {
        PMS.get().infoDbAdd(file, searchFormattedName);
      } else if (isEpisodeToLookup && StringUtils.isNotEmpty(info.ep_name)) {
        formattedName += " - " + info.ep_name;
      } else if (isMovieToLookup && StringUtils.isNotEmpty(info.year)) {
        formattedName += " (" + info.year + ")";
      }
    }

    return formattedName;
  }
  @Override
  public void handle(HttpExchange t) throws IOException {
    if (RemoteUtil.deny(t)) {
      throw new IOException("Access denied");
    }
    RootFolder root = parent.getRoot(RemoteUtil.userName(t), t);
    if (root == null) {
      throw new IOException("Unknown root");
    }
    Headers h = t.getRequestHeaders();
    for (String h1 : h.keySet()) {
      LOGGER.debug("key " + h1 + "=" + h.get(h1));
    }
    String id = RemoteUtil.getId(path, t);
    id = RemoteUtil.strip(id);
    RendererConfiguration r = render;
    if (render == null) {
      r = root.getDefaultRenderer();
    }
    DLNAResource dlna = root.getDLNAResource(id, r);
    if (dlna == null) {
      // another error
      LOGGER.debug("media unkonwn");
      throw new IOException("Bad id");
    }
    if (!dlna.isCodeValid(dlna)) {
      LOGGER.debug("coded object with invalid code");
      throw new IOException("Bad code");
    }
    DLNAMediaSubtitle sid = null;
    long len = dlna.length();
    Range range = RemoteUtil.parseRange(t.getRequestHeaders(), len);
    String mime = root.getDefaultRenderer().getMimeType(dlna.mimeType());
    // DLNAResource dlna = res.get(0);
    WebRender render = (WebRender) r;
    DLNAMediaInfo m = dlna.getMedia();
    if (m == null) {
      m = new DLNAMediaInfo();
      dlna.setMedia(m);
    }
    if (mime.equals(FormatConfiguration.MIMETYPE_AUTO) && m.getMimeType() != null) {
      mime = m.getMimeType();
    }
    int code = 200;
    dlna.setDefaultRenderer(r);
    if (dlna.getFormat().isVideo()) {
      if (flash) {
        mime = "video/flash";
      } else if (!RemoteUtil.directmime(mime) || RemoteUtil.transMp4(mime, m)) {
        mime = render != null ? render.getVideoMimeType() : RemoteUtil.transMime();
        if (FileUtil.isUrl(dlna.getSystemName())) {
          dlna.setPlayer(new FFmpegWebVideo());
        } else {
          dlna.setPlayer(new FFMpegVideo());
        }
        // code = 206;
      }
      if (PMS.getConfiguration().getWebSubs()
          && dlna.getMediaSubtitle() != null
          && dlna.getMediaSubtitle().isExternal()) {
        // fetched on the side
        sid = dlna.getMediaSubtitle();
        dlna.setMediaSubtitle(null);
      }
    }

    if (!RemoteUtil.directmime(mime) && dlna.getFormat().isAudio()) {
      dlna.setPlayer(new FFmpegAudio());
      code = 206;
    }

    m.setMimeType(mime);
    LOGGER.debug("dumping media " + mime + " " + dlna);
    InputStream in = dlna.getInputStream(range, root.getDefaultRenderer());
    Headers hdr = t.getResponseHeaders();
    hdr.add("Content-Type", mime);
    hdr.add("Accept-Ranges", "bytes");
    if (range != null) {
      long end = range.asByteRange().getEnd();
      long start = range.asByteRange().getStart();
      String rStr = start + "-" + end + "/*";
      hdr.add("Content-Range", "bytes " + rStr);
      if (start != 0) {
        code = 206;
      }
    }
    hdr.add("Server", PMS.get().getServerName());
    hdr.add("Connection", "keep-alive");
    t.sendResponseHeaders(code, 0);
    OutputStream os = t.getResponseBody();
    render.start(dlna);
    if (sid != null) {
      dlna.setMediaSubtitle(sid);
    }
    RemoteUtil.dump(in, os, render);
  }
public abstract class Player {
  private static final Logger LOGGER = LoggerFactory.getLogger(Player.class);

  public static final int VIDEO_SIMPLEFILE_PLAYER = 0;
  public static final int AUDIO_SIMPLEFILE_PLAYER = 1;
  public static final int VIDEO_WEBSTREAM_PLAYER = 2;
  public static final int AUDIO_WEBSTREAM_PLAYER = 3;
  public static final int MISC_PLAYER = 4;
  public static final String NATIVE = "NATIVE";

  public abstract int purpose();

  public abstract JComponent config();

  public abstract String id();

  public abstract String name();

  public abstract int type();

  // FIXME this is an implementation detail (and not a very good one).
  // it's entirely up to engines how they construct their command lines.
  // need to get rid of this
  public abstract String[] args();

  public abstract String mimeType();

  public abstract String executable();

  protected static final PmsConfiguration _configuration = PMS.getConfiguration();
  protected PmsConfiguration configuration = _configuration;
  private static List<FinalizeTranscoderArgsListener> finalizeTranscoderArgsListeners =
      new ArrayList<>();

  public static void initializeFinalizeTranscoderArgsListeners() {
    for (ExternalListener listener : ExternalFactory.getExternalListeners()) {
      if (listener instanceof FinalizeTranscoderArgsListener) {
        finalizeTranscoderArgsListeners.add((FinalizeTranscoderArgsListener) listener);
      }
    }
  }

  public boolean avisynth() {
    return false;
  }

  public boolean excludeFormat(Format extension) {
    return false;
  }

  public boolean isPlayerCompatible(RendererConfiguration renderer) {
    return true;
  }

  public boolean isInternalSubtitlesSupported() {
    return true;
  }

  public boolean isExternalSubtitlesSupported() {
    return true;
  }

  public boolean isTimeSeekable() {
    return false;
  }

  /**
   * Each engine capable of video hardware acceleration must override this method and set
   *
   * <p><code>return true</code>.
   *
   * @return false
   */
  public boolean isGPUAccelerationReady() {
    return false;
  }

  /**
   * @deprecated Use {@link #launchTranscode(net.pms.dlna.DLNAResource, net.pms.dlna.DLNAMediaInfo,
   *     net.pms.io.OutputParams)} instead.
   */
  public final ProcessWrapper launchTranscode(
      String filename, DLNAResource dlna, DLNAMediaInfo media, OutputParams params)
      throws IOException {
    return launchTranscode(dlna, media, params);
  }

  public abstract ProcessWrapper launchTranscode(
      DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException;

  @Override
  public String toString() {
    return name();
  }

  // no need to pass Player as a parameter: it's the invocant
  @Deprecated
  protected String[] finalizeTranscoderArgs(
      Player player,
      String filename,
      DLNAResource dlna,
      DLNAMediaInfo media,
      OutputParams params,
      String[] cmdArgs) {
    return finalizeTranscoderArgs(filename, dlna, media, params, cmdArgs);
  }

  protected String[] finalizeTranscoderArgs(
      String filename,
      DLNAResource dlna,
      DLNAMediaInfo media,
      OutputParams params,
      String[] cmdArgs) {
    if (finalizeTranscoderArgsListeners.isEmpty()) {
      return cmdArgs;
    } else {
      // make it mutable
      List<String> cmdList = new ArrayList<>(Arrays.asList(cmdArgs));

      for (FinalizeTranscoderArgsListener listener : finalizeTranscoderArgsListeners) {
        try {
          cmdList = listener.finalizeTranscoderArgs(this, filename, dlna, media, params, cmdList);
        } catch (Throwable t) {
          LOGGER.error(
              String.format(
                  "Failed to call finalizeTranscoderArgs on listener of type=%s",
                  listener.getClass()),
              t);
        }
      }

      String[] cmdArray = new String[cmdList.size()];
      cmdList.toArray(cmdArray);
      return cmdArray;
    }
  }

  /**
   * @deprecated Use {@link #setAudioAndSubs(String fileName, DLNAMediaInfo media, OutputParams
   *     params)} instead.
   */
  public void setAudioAndSubs(
      String fileName, DLNAMediaInfo media, OutputParams params, PmsConfiguration configuration) {
    setAudioAndSubs(fileName, media, params);
  }

  /**
   * This method populates the supplied {@link OutputParams} object with the correct audio track
   * (aid) and subtitles (sid), based on the given filename, its MediaInfo metadata and PMS
   * configuration settings.
   *
   * @param fileName The file name used to determine the availability of subtitles.
   * @param media The MediaInfo metadata for the file.
   * @param params The parameters to populate.
   */
  public static void setAudioAndSubs(String fileName, DLNAMediaInfo media, OutputParams params) {
    setAudioOutputParameters(media, params);
    setSubtitleOutputParameters(fileName, media, params);
  }

  /**
   * This method populates the supplied {@link OutputParams} object with the correct audio track
   * (aid) based on the MediaInfo metadata and PMS configuration settings.
   *
   * @param media The MediaInfo metadata for the file.
   * @param params The parameters to populate.
   */
  public static void setAudioOutputParameters(DLNAMediaInfo media, OutputParams params) {
    PmsConfiguration configuration = PMS.getConfiguration(params);
    if (params.aid == null && media != null && media.getFirstAudioTrack() != null) {
      // check for preferred audio
      DLNAMediaAudio dtsTrack = null;
      StringTokenizer st = new StringTokenizer(configuration.getAudioLanguages(), ",");
      while (st.hasMoreTokens()) {
        String lang = st.nextToken().trim();
        LOGGER.trace("Looking for an audio track with lang: " + lang);
        for (DLNAMediaAudio audio : media.getAudioTracksList()) {
          if (audio.matchCode(lang)) {
            params.aid = audio;
            LOGGER.trace("Matched audio track: " + audio);
            return;
          }

          if (dtsTrack == null && audio.isDTS()) {
            dtsTrack = audio;
          }
        }
      }

      // preferred audio not found, take a default audio track, dts first if available
      if (dtsTrack != null) {
        params.aid = dtsTrack;
        LOGGER.trace("Found priority audio track with DTS: " + dtsTrack);
      } else {
        params.aid = media.getAudioTracksList().get(0);
        LOGGER.trace("Chose a default audio track: " + params.aid);
      }
    }
  }

  /**
   * This method populates the supplied {@link OutputParams} object with the correct subtitles (sid)
   * based on the given filename, its MediaInfo metadata and PMS configuration settings.
   *
   * <p>TODO: Rewrite this crazy method to be more concise and logical.
   *
   * @param fileName The file name used to determine the availability of subtitles.
   * @param media The MediaInfo metadata for the file.
   * @param params The parameters to populate.
   */
  public static void setSubtitleOutputParameters(
      String fileName, DLNAMediaInfo media, OutputParams params) {
    PmsConfiguration configuration = PMS.getConfiguration(params);
    String currentLang = null;
    DLNAMediaSubtitle matchedSub = null;

    if (params.aid != null) {
      currentLang = params.aid.getLang();
    }

    if (params.sid != null && params.sid.getId() == -1) {
      LOGGER.trace("Don't want subtitles!");
      params.sid = null;
      return;
    }

    /** Check for live subtitles */
    if (params.sid != null && !StringUtils.isEmpty(params.sid.getLiveSubURL())) {
      LOGGER.debug("Live subtitles " + params.sid.getLiveSubURL());
      try {
        matchedSub = params.sid;
        String file =
            OpenSubtitle.fetchSubs(matchedSub.getLiveSubURL(), matchedSub.getLiveSubFile());
        if (!StringUtils.isEmpty(file)) {
          matchedSub.setExternalFile(new File(file));
          params.sid = matchedSub;
          return;
        }
      } catch (IOException e) {
      }
    }

    StringTokenizer st = new StringTokenizer(configuration.getAudioSubLanguages(), ";");

    /** Check for external and internal subtitles matching the user's language preferences */
    boolean matchedInternalSubtitles = false;
    boolean matchedExternalSubtitles = false;
    while (st.hasMoreTokens()) {
      String pair = st.nextToken();
      if (pair.contains(",")) {
        String audio = pair.substring(0, pair.indexOf(','));
        String sub = pair.substring(pair.indexOf(',') + 1);
        audio = audio.trim();
        sub = sub.trim();
        LOGGER.trace(
            "Searching for a match for: " + currentLang + " with " + audio + " and " + sub);

        if (Iso639.isCodesMatching(audio, currentLang)
            || (currentLang != null && audio.equals("*"))) {
          if (sub.equals("off")) {
            /**
             * Ignore the "off" language for external subtitles if the user setting is enabled TODO:
             * Prioritize multiple external subtitles properly instead of just taking the first one
             * we load
             */
            if (configuration.isForceExternalSubtitles()) {
              for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
                if (present_sub.getExternalFile() != null) {
                  matchedSub = present_sub;
                  matchedExternalSubtitles = true;
                  LOGGER.trace(
                      "Ignoring the \"off\" language because there are external subtitles");
                  break;
                }
              }
            }
            if (!matchedExternalSubtitles) {
              matchedSub = new DLNAMediaSubtitle();
              matchedSub.setLang("off");
            }
          } else {
            for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
              if (present_sub.matchCode(sub) || sub.equals("*")) {
                if (present_sub.getExternalFile() != null) {
                  if (configuration.isAutoloadExternalSubtitles()) {
                    // Subtitle is external and we want external subtitles, look no further
                    matchedSub = present_sub;
                    LOGGER.trace("Matched external subtitles track: " + matchedSub);
                    break;
                  } else {
                    // Subtitle is external but we do not want external subtitles, keep searching
                    LOGGER.trace(
                        "External subtitles ignored because of user setting: " + present_sub);
                  }
                } else if (!matchedInternalSubtitles) {
                  matchedSub = present_sub;
                  LOGGER.trace("Matched internal subtitles track: " + matchedSub);
                  if (configuration.isAutoloadExternalSubtitles()) {
                    // Subtitle is internal and we will wait to see if an external one is available
                    // instead
                    matchedInternalSubtitles = true;
                  } else {
                    // Subtitle is internal and we will use it
                    break;
                  }
                }
              }
            }
          }

          if (matchedSub != null && !matchedInternalSubtitles) {
            break;
          }
        }
      }
    }

    /**
     * Check for external subtitles that were skipped in the above code block because they didn't
     * match language preferences, if there wasn't already a match and the user settings specify it.
     */
    if (matchedSub == null && configuration.isForceExternalSubtitles()) {
      for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
        if (present_sub.getExternalFile() != null) {
          matchedSub = present_sub;
          LOGGER.trace(
              "Matched external subtitles track that did not match language preferences: "
                  + matchedSub);
          break;
        }
      }
    }

    /**
     * Disable chosen subtitles if the user has disabled all subtitles or if the language
     * preferences have specified the "off" language.
     *
     * <p>TODO: Can't we save a bunch of looping by checking for isDisableSubtitles just after the
     * Live Subtitles check above?
     */
    if (matchedSub != null && params.sid == null) {
      if (configuration.isDisableSubtitles()
          || (matchedSub.getLang() != null && matchedSub.getLang().equals("off"))) {
        LOGGER.trace("Disabled the subtitles: " + matchedSub);
      } else {
        params.sid = matchedSub;
      }
    }

    /** Check for forced subtitles. */
    if (!configuration.isDisableSubtitles() && params.sid == null && media != null) {
      // Check for subtitles again
      File video = new File(fileName);
      FileUtil.isSubtitlesExists(video, media, false);

      if (configuration.isAutoloadExternalSubtitles()) {
        boolean forcedSubsFound = false;
        // Priority to external subtitles
        for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
          if (matchedSub != null
              && matchedSub.getLang() != null
              && matchedSub.getLang().equals("off")) {
            st = new StringTokenizer(configuration.getForcedSubtitleTags(), ",");

            while (sub.getSubtitlesTrackTitleFromMetadata() != null && st.hasMoreTokens()) {
              String forcedTags = st.nextToken();
              forcedTags = forcedTags.trim();

              if (sub.getSubtitlesTrackTitleFromMetadata().toLowerCase().contains(forcedTags)
                  && Iso639.isCodesMatching(
                      sub.getLang(), configuration.getForcedSubtitleLanguage())) {
                LOGGER.trace(
                    "Forcing preferred subtitles: "
                        + sub.getLang()
                        + "/"
                        + sub.getSubtitlesTrackTitleFromMetadata());
                LOGGER.trace("Forced subtitles track: " + sub);

                if (sub.getExternalFile() != null) {
                  LOGGER.trace(
                      "Found external forced file: " + sub.getExternalFile().getAbsolutePath());
                }
                params.sid = sub;
                forcedSubsFound = true;
                break;
              }
            }
            if (forcedSubsFound == true) {
              break;
            }
          } else {
            LOGGER.trace("Found subtitles track: " + sub);

            if (sub.getExternalFile() != null) {
              LOGGER.trace("Found external file: " + sub.getExternalFile().getAbsolutePath());
              params.sid = sub;
              break;
            }
          }
        }
      }
      if (matchedSub != null
          && matchedSub.getLang() != null
          && matchedSub.getLang().equals("off")) {
        return;
      }

      if (params.sid == null) {
        st = new StringTokenizer(UMSUtils.getLangList(params.mediaRenderer), ",");
        while (st.hasMoreTokens()) {
          String lang = st.nextToken();
          lang = lang.trim();
          LOGGER.trace("Looking for a subtitle track with lang: " + lang);
          for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
            if (sub.matchCode(lang)
                && !(!configuration.isAutoloadExternalSubtitles()
                    && sub.getExternalFile() != null)) {
              params.sid = sub;
              LOGGER.trace("Matched subtitles track: " + params.sid);
              return;
            }
          }
        }
      }
    }
  }

  /** @see #convertToModX(int, int) */
  @Deprecated
  public int convertToMod4(int number) {
    return convertToModX(number, 4);
  }

  /**
   * Convert number to be divisible by mod.
   *
   * @param number the number to convert
   * @param mod the number to divide by
   * @return the number divisible by mod
   */
  public static int convertToModX(int number, int mod) {
    if (number % mod != 0) {
      number -= (number % mod);
    }

    return number;
  }

  /**
   * Returns whether or not the player can handle a given resource. If the resource is <code>null
   * </code> compatibility cannot be determined and <code>false</code> will be returned.
   *
   * @param resource The {@link DLNAResource} to be matched.
   * @return True when the resource can be handled, false otherwise.
   * @since 1.60.0
   */
  public abstract boolean isCompatible(DLNAResource resource);

  /**
   * Returns whether or not another player has the same name and id as this one.
   *
   * @param other The other player.
   * @return True if names and ids match, false otherwise.
   */
  @Override
  public boolean equals(Object other) {
    if (other == null || !(other instanceof Player)) {
      return false;
    }
    if (other == this) {
      return true;
    }
    Player otherPlayer = (Player) other;
    return (otherPlayer.name().equals(this.name()) && otherPlayer.id().equals(this.id()));
  }
}
  /**
   * This method populates the supplied {@link OutputParams} object with the correct subtitles (sid)
   * based on the given filename, its MediaInfo metadata and PMS configuration settings.
   *
   * <p>TODO: Rewrite this crazy method to be more concise and logical.
   *
   * @param fileName The file name used to determine the availability of subtitles.
   * @param media The MediaInfo metadata for the file.
   * @param params The parameters to populate.
   */
  public static void setSubtitleOutputParameters(
      String fileName, DLNAMediaInfo media, OutputParams params) {
    PmsConfiguration configuration = PMS.getConfiguration(params);
    String currentLang = null;
    DLNAMediaSubtitle matchedSub = null;

    if (params.aid != null) {
      currentLang = params.aid.getLang();
    }

    if (params.sid != null && params.sid.getId() == -1) {
      LOGGER.trace("Don't want subtitles!");
      params.sid = null;
      return;
    }

    /** Check for live subtitles */
    if (params.sid != null && !StringUtils.isEmpty(params.sid.getLiveSubURL())) {
      LOGGER.debug("Live subtitles " + params.sid.getLiveSubURL());
      try {
        matchedSub = params.sid;
        String file =
            OpenSubtitle.fetchSubs(matchedSub.getLiveSubURL(), matchedSub.getLiveSubFile());
        if (!StringUtils.isEmpty(file)) {
          matchedSub.setExternalFile(new File(file));
          params.sid = matchedSub;
          return;
        }
      } catch (IOException e) {
      }
    }

    StringTokenizer st = new StringTokenizer(configuration.getAudioSubLanguages(), ";");

    /** Check for external and internal subtitles matching the user's language preferences */
    boolean matchedInternalSubtitles = false;
    boolean matchedExternalSubtitles = false;
    while (st.hasMoreTokens()) {
      String pair = st.nextToken();
      if (pair.contains(",")) {
        String audio = pair.substring(0, pair.indexOf(','));
        String sub = pair.substring(pair.indexOf(',') + 1);
        audio = audio.trim();
        sub = sub.trim();
        LOGGER.trace(
            "Searching for a match for: " + currentLang + " with " + audio + " and " + sub);

        if (Iso639.isCodesMatching(audio, currentLang)
            || (currentLang != null && audio.equals("*"))) {
          if (sub.equals("off")) {
            /**
             * Ignore the "off" language for external subtitles if the user setting is enabled TODO:
             * Prioritize multiple external subtitles properly instead of just taking the first one
             * we load
             */
            if (configuration.isForceExternalSubtitles()) {
              for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
                if (present_sub.getExternalFile() != null) {
                  matchedSub = present_sub;
                  matchedExternalSubtitles = true;
                  LOGGER.trace(
                      "Ignoring the \"off\" language because there are external subtitles");
                  break;
                }
              }
            }
            if (!matchedExternalSubtitles) {
              matchedSub = new DLNAMediaSubtitle();
              matchedSub.setLang("off");
            }
          } else {
            for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
              if (present_sub.matchCode(sub) || sub.equals("*")) {
                if (present_sub.getExternalFile() != null) {
                  if (configuration.isAutoloadExternalSubtitles()) {
                    // Subtitle is external and we want external subtitles, look no further
                    matchedSub = present_sub;
                    LOGGER.trace("Matched external subtitles track: " + matchedSub);
                    break;
                  } else {
                    // Subtitle is external but we do not want external subtitles, keep searching
                    LOGGER.trace(
                        "External subtitles ignored because of user setting: " + present_sub);
                  }
                } else if (!matchedInternalSubtitles) {
                  matchedSub = present_sub;
                  LOGGER.trace("Matched internal subtitles track: " + matchedSub);
                  if (configuration.isAutoloadExternalSubtitles()) {
                    // Subtitle is internal and we will wait to see if an external one is available
                    // instead
                    matchedInternalSubtitles = true;
                  } else {
                    // Subtitle is internal and we will use it
                    break;
                  }
                }
              }
            }
          }

          if (matchedSub != null && !matchedInternalSubtitles) {
            break;
          }
        }
      }
    }

    /**
     * Check for external subtitles that were skipped in the above code block because they didn't
     * match language preferences, if there wasn't already a match and the user settings specify it.
     */
    if (matchedSub == null && configuration.isForceExternalSubtitles()) {
      for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
        if (present_sub.getExternalFile() != null) {
          matchedSub = present_sub;
          LOGGER.trace(
              "Matched external subtitles track that did not match language preferences: "
                  + matchedSub);
          break;
        }
      }
    }

    /**
     * Disable chosen subtitles if the user has disabled all subtitles or if the language
     * preferences have specified the "off" language.
     *
     * <p>TODO: Can't we save a bunch of looping by checking for isDisableSubtitles just after the
     * Live Subtitles check above?
     */
    if (matchedSub != null && params.sid == null) {
      if (configuration.isDisableSubtitles()
          || (matchedSub.getLang() != null && matchedSub.getLang().equals("off"))) {
        LOGGER.trace("Disabled the subtitles: " + matchedSub);
      } else {
        params.sid = matchedSub;
      }
    }

    /** Check for forced subtitles. */
    if (!configuration.isDisableSubtitles() && params.sid == null && media != null) {
      // Check for subtitles again
      File video = new File(fileName);
      FileUtil.isSubtitlesExists(video, media, false);

      if (configuration.isAutoloadExternalSubtitles()) {
        boolean forcedSubsFound = false;
        // Priority to external subtitles
        for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
          if (matchedSub != null
              && matchedSub.getLang() != null
              && matchedSub.getLang().equals("off")) {
            st = new StringTokenizer(configuration.getForcedSubtitleTags(), ",");

            while (sub.getSubtitlesTrackTitleFromMetadata() != null && st.hasMoreTokens()) {
              String forcedTags = st.nextToken();
              forcedTags = forcedTags.trim();

              if (sub.getSubtitlesTrackTitleFromMetadata().toLowerCase().contains(forcedTags)
                  && Iso639.isCodesMatching(
                      sub.getLang(), configuration.getForcedSubtitleLanguage())) {
                LOGGER.trace(
                    "Forcing preferred subtitles: "
                        + sub.getLang()
                        + "/"
                        + sub.getSubtitlesTrackTitleFromMetadata());
                LOGGER.trace("Forced subtitles track: " + sub);

                if (sub.getExternalFile() != null) {
                  LOGGER.trace(
                      "Found external forced file: " + sub.getExternalFile().getAbsolutePath());
                }
                params.sid = sub;
                forcedSubsFound = true;
                break;
              }
            }
            if (forcedSubsFound == true) {
              break;
            }
          } else {
            LOGGER.trace("Found subtitles track: " + sub);

            if (sub.getExternalFile() != null) {
              LOGGER.trace("Found external file: " + sub.getExternalFile().getAbsolutePath());
              params.sid = sub;
              break;
            }
          }
        }
      }
      if (matchedSub != null
          && matchedSub.getLang() != null
          && matchedSub.getLang().equals("off")) {
        return;
      }

      if (params.sid == null) {
        st = new StringTokenizer(UMSUtils.getLangList(params.mediaRenderer), ",");
        while (st.hasMoreTokens()) {
          String lang = st.nextToken();
          lang = lang.trim();
          LOGGER.trace("Looking for a subtitle track with lang: " + lang);
          for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
            if (sub.matchCode(lang)
                && !(!configuration.isAutoloadExternalSubtitles()
                    && sub.getExternalFile() != null)) {
              params.sid = sub;
              LOGGER.trace("Matched subtitles track: " + params.sid);
              return;
            }
          }
        }
      }
    }
  }
 @Override
 public String executable() {
   return PMS.getConfiguration().getFfmpegPath();
 }
@SuppressWarnings("restriction")
public class RemoteWeb {
  private static final Logger LOGGER = LoggerFactory.getLogger(RemoteWeb.class);
  private KeyStore ks;
  private KeyManagerFactory kmf;
  private TrustManagerFactory tmf;
  private HttpServer server;
  private SSLContext sslContext;
  private HashMap<String, String> users;
  private HashMap<String, String> tags;
  private Map<String, RootFolder> roots;
  private RemoteUtil.ResourceManager resources;
  private static final PmsConfiguration configuration = PMS.getConfiguration();
  private static final int defaultPort = configuration.getWebPort();

  public RemoteWeb() throws IOException {
    this(defaultPort);
  }

  public RemoteWeb(int port) throws IOException {
    if (port <= 0) {
      port = defaultPort;
    }

    users = new HashMap<>();
    tags = new HashMap<>();
    roots = new HashMap<String, RootFolder>();
    // Add "classpaths" for resolving web resources
    resources =
        AccessController.doPrivileged(
            new PrivilegedAction<RemoteUtil.ResourceManager>() {

              public RemoteUtil.ResourceManager run() {
                return new RemoteUtil.ResourceManager(
                    "file:" + configuration.getProfileDirectory() + "/web/",
                    "jar:file:" + configuration.getProfileDirectory() + "/web.zip!/",
                    "file:" + configuration.getWebPath() + "/");
              }
            });

    readCred();

    // Setup the socket address
    InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("0.0.0.0"), port);

    // Initialize the HTTP(S) server
    if (configuration.getWebHttps()) {
      try {
        server = httpsServer(address);
      } catch (IOException e) {
        LOGGER.error("Failed to start WEB interface on HTTPS: {}", e.getMessage());
        LOGGER.trace("", e);
        if (e.getMessage().contains("UMS.jks")) {
          LOGGER.info(
              "To enable HTTPS please generate a self-signed keystore file called \"UMS.jks\" using the java 'keytool' commandline utility.");
        }
      } catch (GeneralSecurityException e) {
        LOGGER.error(
            "Failed to start WEB interface on HTTPS due to a security error: {}", e.getMessage());
        LOGGER.trace("", e);
      }
    } else {
      server = HttpServer.create(address, 0);
    }

    if (server != null) {
      int threads = configuration.getWebThreads();

      // Add context handlers
      addCtx("/", new RemoteStartHandler(this));
      addCtx("/browse", new RemoteBrowseHandler(this));
      RemotePlayHandler playHandler = new RemotePlayHandler(this);
      addCtx("/play", playHandler);
      addCtx("/playstatus", playHandler);
      addCtx("/playlist", playHandler);
      addCtx("/media", new RemoteMediaHandler(this));
      addCtx("/fmedia", new RemoteMediaHandler(this, true));
      addCtx("/thumb", new RemoteThumbHandler(this));
      addCtx("/raw", new RemoteRawHandler(this));
      addCtx("/files", new RemoteFileHandler(this));
      addCtx("/doc", new RemoteDocHandler(this));
      addCtx("/poll", new RemotePollHandler(this));
      server.setExecutor(Executors.newFixedThreadPool(threads));
      server.start();
    }
  }

  private HttpServer httpsServer(InetSocketAddress address)
      throws IOException, GeneralSecurityException {
    // Initialize the keystore
    char[] password = "******".toCharArray();
    ks = KeyStore.getInstance("JKS");
    try (FileInputStream fis = new FileInputStream("UMS.jks")) {
      ks.load(fis, password);
    }

    // Setup the key manager factory
    kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, password);

    // Setup the trust manager factory
    tmf = TrustManagerFactory.getInstance("SunX509");
    tmf.init(ks);

    HttpsServer server = HttpsServer.create(address, 0);
    sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

    server.setHttpsConfigurator(
        new HttpsConfigurator(sslContext) {
          @Override
          public void configure(HttpsParameters params) {
            try {
              // initialise the SSL context
              SSLContext c = SSLContext.getDefault();
              SSLEngine engine = c.createSSLEngine();
              params.setNeedClientAuth(true);
              params.setCipherSuites(engine.getEnabledCipherSuites());
              params.setProtocols(engine.getEnabledProtocols());

              // get the default parameters
              SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();
              params.setSSLParameters(defaultSSLParameters);
            } catch (Exception e) {
              LOGGER.debug("https configure error  " + e);
            }
          }
        });
    return server;
  }

  public String getTag(String user) {
    String tag = tags.get(user);
    if (tag == null) {
      return user;
    }
    return tag;
  }

  public String getAddress() {
    return PMS.get().getServer().getHost() + ":" + server.getAddress().getPort();
  }

  public RootFolder getRoot(String user, HttpExchange t) {
    return getRoot(user, false, t);
  }

  public RootFolder getRoot(String user, boolean create, HttpExchange t) {
    String groupTag = getTag(user);
    String cookie = RemoteUtil.getCookie("UMS", t);
    RootFolder root;
    synchronized (roots) {
      root = roots.get(cookie);
      if (root == null) {
        // Double-check for cookie errors
        WebRender valid = RemoteUtil.matchRenderer(user, t);
        if (valid != null) {
          // A browser of the same type and user is already connected at
          // this ip but for some reason we didn't get a cookie match.
          RootFolder validRoot = valid.getRootFolder();
          // Do a reverse lookup to see if it's been registered
          for (Map.Entry<String, RootFolder> entry : roots.entrySet()) {
            if (entry.getValue() == validRoot) {
              // Found
              root = validRoot;
              cookie = entry.getKey();
              LOGGER.debug(
                  "Allowing browser connection without cookie match: {}: {}",
                  valid.getRendererName(),
                  t.getRemoteAddress().getAddress());
              break;
            }
          }
        }
      }

      if (!create || (root != null)) {
        t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/");
        return root;
      }

      ArrayList<String> tag = new ArrayList<>();
      tag.add(user);
      if (!groupTag.equals(user)) {
        tag.add(groupTag);
      }

      tag.add(t.getRemoteAddress().getHostString());
      tag.add("web");
      root = new RootFolder(tag);
      try {
        WebRender render = new WebRender(user);
        root.setDefaultRenderer(render);
        render.setRootFolder(root);
        render.associateIP(t.getRemoteAddress().getAddress());
        render.associatePort(t.getRemoteAddress().getPort());
        if (configuration.useWebSubLang()) {
          render.setSubLang(StringUtils.join(RemoteUtil.getLangs(t), ","));
        }
        //				render.setUA(t.getRequestHeaders().getFirst("User-agent"));
        render.setBrowserInfo(
            RemoteUtil.getCookie("UMSINFO", t), t.getRequestHeaders().getFirst("User-agent"));
        PMS.get().setRendererFound(render);
      } catch (ConfigurationException e) {
        root.setDefaultRenderer(RendererConfiguration.getDefaultConf());
      }
      // root.setDefaultRenderer(RendererConfiguration.getRendererConfigurationByName("web"));
      root.discoverChildren();
      cookie = UUID.randomUUID().toString();
      t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/");
      roots.put(cookie, root);
    }
    return root;
  }

  public void associate(HttpExchange t, WebRender webRenderer) {
    webRenderer.associateIP(t.getRemoteAddress().getAddress());
    webRenderer.associatePort(t.getRemoteAddress().getPort());
  }

  private void addCtx(String path, HttpHandler h) {
    HttpContext ctx = server.createContext(path, h);
    if (configuration.isWebAuthenticate()) {
      ctx.setAuthenticator(
          new BasicAuthenticator("") {
            @Override
            public boolean checkCredentials(String user, String pwd) {
              LOGGER.debug("authenticate " + user);
              return pwd.equals(users.get(user));
              // return true;
            }
          });
    }
  }

  private void readCred() throws IOException {
    String cPath = (String) configuration.getCustomProperty("cred.path");
    if (StringUtils.isEmpty(cPath)) {
      return;
    }
    File f = new File(cPath);
    if (!f.exists()) {
      return;
    }
    try (BufferedReader in =
        new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
      String str;
      while ((str = in.readLine()) != null) {
        str = str.trim();
        if (StringUtils.isEmpty(str) || str.startsWith("#")) {
          continue;
        }
        String[] s = str.split("\\s*=\\s*", 2);
        if (s.length < 2) {
          continue;
        }
        if (!s[0].startsWith("web")) {
          continue;
        }
        String[] s1 = s[0].split("\\.", 2);
        String[] s2 = s[1].split(",", 2);
        if (s2.length < 2) {
          continue;
        }
        // s2[0] == usr s2[1] == pwd s1[1] == tag
        users.put(s2[0], s2[1]);
        if (s1.length > 1) {
          // there is a tag here
          tags.put(s2[0], s1[1]);
        }
      }
    }
  }

  public HttpServer getServer() {
    return server;
  }

  static class RemoteThumbHandler implements HttpHandler {
    private RemoteWeb parent;

    public RemoteThumbHandler(RemoteWeb parent) {
      this.parent = parent;
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
      if (RemoteUtil.deny(t)) {
        throw new IOException("Access denied");
      }
      String id = RemoteUtil.getId("thumb/", t);
      LOGGER.trace("web thumb req " + id);
      if (id.contains("logo")) {
        RemoteUtil.sendLogo(t);
        return;
      }
      RootFolder root = parent.getRoot(RemoteUtil.userName(t), t);
      if (root == null) {
        LOGGER.debug("weird root in thumb req");
        throw new IOException("Unknown root");
      }
      final DLNAResource r = root.getDLNAResource(id, root.getDefaultRenderer());
      if (r == null) {
        // another error
        LOGGER.debug("media unknown");
        throw new IOException("Bad id");
      }
      InputStream in;
      if (!configuration.isShowCodeThumbs() && !r.isCodeValid(r)) {
        // we shouldn't show the thumbs for coded objects
        // unless the code is entered
        in = r.getGenericThumbnailInputStream(null);
      } else {
        r.checkThumbnail();
        in = r.getThumbnailInputStream();
      }
      Headers hdr = t.getResponseHeaders();
      hdr.add("Content-Type", r.getThumbnailContentType());
      hdr.add("Accept-Ranges", "bytes");
      hdr.add("Connection", "keep-alive");
      t.sendResponseHeaders(200, in.available());
      OutputStream os = t.getResponseBody();
      LOGGER.trace("input is {} output is {}", in, os);
      RemoteUtil.dump(in, os);
    }
  }

  static class RemoteFileHandler implements HttpHandler {
    private RemoteWeb parent;

    public RemoteFileHandler(RemoteWeb parent) {
      this.parent = parent;
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
      LOGGER.debug("file req " + t.getRequestURI());

      String path = t.getRequestURI().getPath();
      String response = null;
      String mime = null;
      int status = 200;

      if (path.contains("crossdomain.xml")) {
        response =
            "<?xml version=\"1.0\"?>"
                + "<!-- http://www.bitsontherun.com/crossdomain.xml -->"
                + "<cross-domain-policy>"
                + "<allow-access-from domain=\"*\" />"
                + "</cross-domain-policy>";
        mime = "text/xml";

      } else if (path.startsWith("/files/log/")) {
        String filename = path.substring(11);
        if (filename.equals("info")) {
          String log = PMS.get().getFrame().getLog();
          log = log.replaceAll("\n", "<br>");
          String fullLink = "<br><a href=\"/files/log/full\">Full log</a><br><br>";
          String x = fullLink + log;
          if (StringUtils.isNotEmpty(log)) {
            x = x + fullLink;
          }
          response = "<html><title>UMS LOG</title><body>" + x + "</body></html>";
        } else {
          File file = parent.getResources().getFile(filename);
          if (file != null) {
            filename = file.getName();
            HashMap<String, Object> vars = new HashMap<>();
            vars.put("title", filename);
            vars.put(
                "brush",
                filename.endsWith("debug.log")
                    ? "debug_log"
                    : filename.endsWith(".log") ? "log" : "conf");
            vars.put("log", RemoteUtil.read(file).replace("<", "&lt;"));
            response = parent.getResources().getTemplate("util/log.html").execute(vars);
          } else {
            status = 404;
          }
        }
        mime = "text/html";

      } else if (parent.getResources().write(path.substring(7), t)) {
        // The resource manager found and sent the file, all done.
        return;

      } else {
        status = 404;
      }

      if (status == 404 && response == null) {
        response = "<html><body>404 - File Not Found: " + path + "</body></html>";
        mime = "text/html";
      }

      RemoteUtil.respond(t, response, status, mime);
    }
  }

  static class RemoteStartHandler implements HttpHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(RemoteStartHandler.class);

    @SuppressWarnings("unused")
    private static final String CRLF = "\r\n";

    private RemoteWeb parent;

    public RemoteStartHandler(RemoteWeb parent) {
      this.parent = parent;
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
      LOGGER.debug("root req " + t.getRequestURI());
      if (RemoteUtil.deny(t)) {
        throw new IOException("Access denied");
      }
      if (t.getRequestURI().getPath().contains("favicon")) {
        RemoteUtil.sendLogo(t);
        return;
      }

      HashMap<String, Object> vars = new HashMap<>();
      vars.put("serverName", configuration.getServerName());
      vars.put("profileName", configuration.getProfileName());

      String response = parent.getResources().getTemplate("start.html").execute(vars);
      RemoteUtil.respond(t, response, 200, "text/html");
    }
  }

  static class RemoteDocHandler implements HttpHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(RemoteDocHandler.class);

    @SuppressWarnings("unused")
    private static final String CRLF = "\r\n";

    private RemoteWeb parent;

    public RemoteDocHandler(RemoteWeb parent) {
      this.parent = parent;
      // Make sure logs are available right away
      getLogs(false);
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
      LOGGER.debug("root req " + t.getRequestURI());
      if (RemoteUtil.deny(t)) {
        throw new IOException("Access denied");
      }
      if (t.getRequestURI().getPath().contains("favicon")) {
        RemoteUtil.sendLogo(t);
        return;
      }

      HashMap<String, Object> vars = new HashMap<>();
      vars.put("logs", getLogs(true));
      if (configuration.getUseCache()) {
        vars.put(
            "cache",
            "http://"
                + PMS.get().getServer().getHost()
                + ":"
                + PMS.get().getServer().getPort()
                + "/console/home");
      }

      String response = parent.getResources().getTemplate("doc.html").execute(vars);
      RemoteUtil.respond(t, response, 200, "text/html");
    }

    private ArrayList<HashMap<String, String>> getLogs(boolean asList) {
      Set<File> files = new DbgPacker().getItems();
      ArrayList<HashMap<String, String>> logs =
          asList ? new ArrayList<HashMap<String, String>>() : null;
      for (File f : files) {
        if (f.exists()) {
          String id = String.valueOf(parent.getResources().add(f));
          if (asList) {
            HashMap<String, String> item = new HashMap<>();
            item.put("filename", f.getName());
            item.put("id", id);
            logs.add(item);
          }
        }
      }
      return logs;
    }
  }

  public RemoteUtil.ResourceManager getResources() {
    return resources;
  }

  public String getUrl() {
    if (server != null) {
      return (server instanceof HttpsServer ? "https://" : "http://")
          + PMS.get().getServer().getHost()
          + ":"
          + server.getAddress().getPort();
    }
    return null;
  }

  static class RemotePollHandler implements HttpHandler {
    @SuppressWarnings("unused")
    private static final Logger LOGGER = LoggerFactory.getLogger(RemotePollHandler.class);

    @SuppressWarnings("unused")
    private static final String CRLF = "\r\n";

    private RemoteWeb parent;

    public RemotePollHandler(RemoteWeb parent) {
      this.parent = parent;
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
      // LOGGER.debug("poll req " + t.getRequestURI());
      if (RemoteUtil.deny(t)) {
        throw new IOException("Access denied");
      }
      RootFolder root = parent.getRoot(RemoteUtil.userName(t), t);
      WebRender renderer = (WebRender) root.getDefaultRenderer();
      String json = renderer.getPushData();
      RemoteUtil.respond(t, json, 200, "text");
    }
  }
}
 /**
  * Applies the IP filter to the specified internet address. Returns true if the address is not
  * allowed and therefore should be filtered out, false otherwise.
  *
  * @param inetAddress The internet address to verify.
  * @return True when not allowed, false otherwise.
  */
 private boolean filterIp(InetAddress inetAddress) {
   return !PMS.getConfiguration().getIpFiltering().allowed(inetAddress);
 }
public class WEB extends Format {
  private static final PmsConfiguration configuration = PMS.getConfiguration();

  /** {@inheritDoc} */
  @Override
  public Identifier getIdentifier() {
    return Identifier.WEB;
  }

  /**
   * @deprecated Use {@link #isCompatible(DLNAMediaInfo, RendererConfiguration)} instead.
   *     <p>Returns whether or not a format can be handled by the PS3 natively. This means the
   *     format can be streamed to PS3 instead of having to be transcoded.
   * @return True if the format can be handled by PS3, false otherwise.
   */
  @Deprecated
  @Override
  public boolean ps3compatible() {
    return type == IMAGE;
  }

  @Override
  public ArrayList<Class<? extends Player>> getProfiles() {
    ArrayList<Class<? extends Player>> a = new ArrayList<>();
    if (type == AUDIO) {
      PMS r = PMS.get();
      for (String engine : configuration.getEnginesAsList(r.getRegistry())) {
        switch (engine) {
          case MPlayerWebAudio.ID:
            a.add(MPlayerWebAudio.class);
            break;
          case VideoLanAudioStreaming.ID:
            a.add(VideoLanAudioStreaming.class);
            break;
        }
      }
    } else {
      PMS r = PMS.get();
      for (String engine : configuration.getEnginesAsList(r.getRegistry())) {
        switch (engine) {
          case FFmpegWebVideo.ID:
            a.add(FFmpegWebVideo.class);
            break;
          case MEncoderWebVideo.ID:
            a.add(MEncoderWebVideo.class);
            break;
          case VideoLanVideoStreaming.ID:
            a.add(VideoLanVideoStreaming.class);
            break;
          case MPlayerWebVideoDump.ID:
            a.add(MPlayerWebVideoDump.class);
            break;
        }
      }
    }

    return a;
  }

  /** {@inheritDoc} */
  @Override
  // TODO remove screen - it's been tried numerous times (see forum) and it doesn't work
  public String[] getId() {
    return new String[] {
      "http", "mms", "mmsh", "mmst", "rtsp", "rtp", "udp", "screen", "rtmp", "https"
    };
  }

  @Override
  public boolean transcodable() {
    return true;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isCompatible(DLNAMediaInfo media, RendererConfiguration renderer) {
    // Emulating ps3compatible()
    return type == IMAGE;
  }
}
/**
 * This class takes care of registering plugins. Plugin jars are loaded, instantiated and stored for
 * later retrieval.
 */
public class ExternalFactory {
  /** For logging messages. */
  private static final Logger LOGGER = LoggerFactory.getLogger(ExternalFactory.class);

  private static final PmsConfiguration configuration = PMS.getConfiguration();

  /** List of external listener class instances. */
  private static List<ExternalListener> externalListeners = new ArrayList<>();

  /** List of external listener classes. */
  private static List<Class<?>> externalListenerClasses = new ArrayList<>();

  /** List of external listener classes (not yet started). */
  private static List<Class<?>> downloadedListenerClasses = new ArrayList<>();

  /** List of urlresolvers. */
  private static List<URLResolver> urlResolvers = new ArrayList<>();

  private static boolean allDone = false;

  /**
   * Returns the list of external listener class instances.
   *
   * @return The instances.
   */
  public static List<ExternalListener> getExternalListeners() {
    return externalListeners;
  }

  /**
   * Stores the instance of an external listener in a list for later retrieval. The same instance
   * will only be stored once.
   *
   * @param listener The instance to store.
   */
  public static void registerListener(ExternalListener listener) {
    if (!externalListeners.contains(listener)) {
      externalListeners.add(listener);
      if (listener instanceof URLResolver) {
        addURLResolver((URLResolver) listener);
      }
    }
  }

  /**
   * Stores the class of an external listener in a list for later retrieval. The same class will
   * only be stored once.
   *
   * @param clazz The class to store.
   */
  private static void registerListenerClass(Class<?> clazz) {
    if (!externalListenerClasses.contains(clazz)) {
      externalListenerClasses.add(clazz);
    }
  }

  private static String getMainClass(URL jar) {
    URL[] jarURLs1 = {jar};
    URLClassLoader classLoader = new URLClassLoader(jarURLs1);
    Enumeration<URL> resources;

    try {
      // Each plugin .jar file has to contain a resource named "plugin"
      // which should contain the name of the main plugin class.
      resources = classLoader.getResources("plugin");

      if (resources.hasMoreElements()) {
        URL url = resources.nextElement();
        char[] name;

        // Determine the plugin main class name from the contents of
        // the plugin file.
        try (InputStreamReader in = new InputStreamReader(url.openStream())) {
          name = new char[512];
          in.read(name);
        }

        return new String(name).trim();
      }
    } catch (IOException e) {
      LOGGER.error("Can't load plugin resources", e);
    }

    return null;
  }

  private static boolean isLib(URL jar) {
    return (getMainClass(jar) == null);
  }

  public static void loadJARs(URL[] jarURLs, boolean download) {
    // find lib jars first
    ArrayList<URL> libs = new ArrayList<>();

    for (URL jarURL : jarURLs) {
      if (isLib(jarURL)) {
        libs.add(jarURL);
      }
    }

    URL[] jarURLs1 = new URL[libs.size() + 1];
    libs.toArray(jarURLs1);
    int pos = libs.size();

    for (URL jarURL : jarURLs) {
      jarURLs1[pos] = jarURL;
      loadJAR(jarURLs1, download, jarURL);
    }
  }

  /** This method loads the jar files found in the plugin dir or if installed from the web. */
  public static void loadJAR(URL[] jarURL, boolean download, URL newURL) {
    // Create a classloader to take care of loading the plugin classes from
    // their URL.
    URLClassLoader classLoader = new URLClassLoader(jarURL);
    Enumeration<URL> resources;

    try {
      // Each plugin .jar file has to contain a resource named "plugin"
      // which should contain the name of the main plugin class.
      resources = classLoader.getResources("plugin");
    } catch (IOException e) {
      LOGGER.error("Can't load plugin resources", e);
      return;
    }

    while (resources.hasMoreElements()) {
      URL url = resources.nextElement();

      try {
        // Determine the plugin main class name from the contents of
        // the plugin file.
        char[] name;
        try (InputStreamReader in = new InputStreamReader(url.openStream())) {
          name = new char[512];
          in.read(name);
        }
        String pluginMainClassName = new String(name).trim();

        LOGGER.info("Found plugin: " + pluginMainClassName);

        if (download) {
          // Only purge code when downloading!
          purgeCode(pluginMainClassName, newURL);
        }

        // Try to load the class based on the main class name
        Class<?> clazz = classLoader.loadClass(pluginMainClassName);
        registerListenerClass(clazz);

        if (download) {
          downloadedListenerClasses.add(clazz);
        }
      } catch (Exception | NoClassDefFoundError e) {
        LOGGER.error("Error loading plugin", e);
      }
    }
  }

  private static void purgeCode(String mainClass, URL newUrl) {
    Class<?> clazz1 = null;

    for (Class<?> clazz : externalListenerClasses) {
      if (mainClass.equals(clazz.getCanonicalName())) {
        clazz1 = clazz;
        break;
      }
    }

    if (clazz1 == null) {
      return;
    }

    externalListenerClasses.remove(clazz1);
    ExternalListener remove = null;
    for (ExternalListener list : externalListeners) {
      if (list.getClass().equals(clazz1)) {
        remove = list;
        break;
      }
    }

    RendererConfiguration.resetAllRenderers();

    if (remove != null) {
      externalListeners.remove(remove);
      remove.shutdown();
      LooksFrame frame = (LooksFrame) PMS.get().getFrame();
      frame.getPt().removePlugin(remove);
    }

    for (int i = 0; i < 3; i++) {
      System.gc();
    }

    URLClassLoader cl = (URLClassLoader) clazz1.getClassLoader();
    URL[] urls = cl.getURLs();
    for (URL url : urls) {
      String mainClass1 = getMainClass(url);

      if (mainClass1 == null || !mainClass.equals(mainClass1)) {
        continue;
      }

      File f = url2file(url);
      File f1 = url2file(newUrl);

      if (f1 == null || f == null) {
        continue;
      }

      if (!f1.getName().equals(f.getName())) {
        addToPurgeFile(f);
      }
    }
  }

  private static File url2file(URL url) {
    File f;

    try {
      f = new File(url.toURI());
    } catch (URISyntaxException e) {
      f = new File(url.getPath());
    }

    return f;
  }

  private static void addToPurgeFile(File f) {
    try {
      try (FileWriter out = new FileWriter("purge", true)) {
        out.write(f.getAbsolutePath() + "\r\n");
        out.flush();
      }
    } catch (Exception e) {
      LOGGER.debug("purge file error " + e);
    }
  }

  private static void purgeFiles() {
    File purge = new File("purge");
    String action = configuration.getPluginPurgeAction();

    if (action.equalsIgnoreCase("none")) {
      purge.delete();
      return;
    }

    try {
      try (FileInputStream fis = new FileInputStream(purge);
          BufferedReader in = new BufferedReader(new InputStreamReader(fis))) {
        String line;

        while ((line = in.readLine()) != null) {
          File f = new File(line);

          if (action.equalsIgnoreCase("delete")) {
            f.delete();
          } else if (action.equalsIgnoreCase("backup")) {
            FileUtils.moveFileToDirectory(f, new File("backup"), true);
            f.delete();
          }
        }
      }
    } catch (IOException e) {
    }
    purge.delete();
  }

  /**
   * This method scans the plugins directory for ".jar" files and processes each file that is found.
   * First, a resource named "plugin" is extracted from the jar file. Its contents determine the
   * name of the main plugin class. This main plugin class is then loaded and an instance is created
   * and registered for later use.
   */
  public static void lookup() {
    // Start by purging files
    purgeFiles();
    File pluginDirectory = new File(configuration.getPluginDirectory());
    LOGGER.info("Searching for plugins in " + pluginDirectory.getAbsolutePath());

    if (!pluginDirectory.exists()) {
      LOGGER.warn("Plugin directory doesn't exist: " + pluginDirectory);
      return;
    }

    if (!pluginDirectory.isDirectory()) {
      LOGGER.warn("Plugin directory is not a directory: " + pluginDirectory);
      return;
    }

    if (!pluginDirectory.canRead()) {
      LOGGER.warn("Plugin directory is not readable: " + pluginDirectory);
      return;
    }

    // Find all .jar files in the plugin directory
    File[] jarFiles =
        pluginDirectory.listFiles(
            new FileFilter() {
              @Override
              public boolean accept(File file) {
                return file.isFile() && file.getName().toLowerCase().endsWith(".jar");
              }
            });

    int nJars = (jarFiles == null) ? 0 : jarFiles.length;

    if (nJars == 0) {
      LOGGER.info("No plugins found");
      return;
    }

    // To load a .jar file the filename needs to converted to a file URL
    List<URL> jarURLList = new ArrayList<>();

    for (int i = 0; i < nJars; ++i) {
      try {
        jarURLList.add(jarFiles[i].toURI().toURL());
      } catch (MalformedURLException e) {
        LOGGER.error("Can't convert file path " + jarFiles[i] + " to URL", e);
      }
    }

    URL[] jarURLs = new URL[jarURLList.size()];
    jarURLList.toArray(jarURLs);

    // Load the jars
    loadJARs(jarURLs, false);

    // Instantiate the early external listeners immediately.
    instantiateEarlyListeners();
  }

  /**
   * This method instantiates the external listeners that need to be instantiated immediately so
   * they can influence the PMS initialization process.
   *
   * <p>Not all external listeners are instantiated immediately to avoid premature initialization
   * where other parts of PMS have not been initialized yet. Those listeners are instantiated at a
   * later time by {@link #instantiateLateListeners()}.
   */
  private static void instantiateEarlyListeners() {
    for (Class<?> clazz : externalListenerClasses) {
      // Skip the classes that should not be instantiated at this
      // time but rather at a later time.
      if (!AdditionalFolderAtRoot.class.isAssignableFrom(clazz)
          && !AdditionalFoldersAtRoot.class.isAssignableFrom(clazz)) {
        try {
          // Create a new instance of the plugin class and store it
          ExternalListener instance = (ExternalListener) clazz.newInstance();
          registerListener(instance);
        } catch (InstantiationException | IllegalAccessException e) {
          LOGGER.error("Error instantiating plugin", e);
        }
      }
    }
  }

  /**
   * This method instantiates the external listeners whose class has not yet been instantiated by
   * {@link #instantiateEarlyListeners()}.
   */
  public static void instantiateLateListeners() {
    for (Class<?> clazz : externalListenerClasses) {
      // Only AdditionalFolderAtRoot and AdditionalFoldersAtRoot
      // classes have been skipped by lookup().
      if (AdditionalFolderAtRoot.class.isAssignableFrom(clazz)
          || AdditionalFoldersAtRoot.class.isAssignableFrom(clazz)) {
        try {
          // Create a new instance of the plugin class and store it
          ExternalListener instance = (ExternalListener) clazz.newInstance();
          registerListener(instance);
        } catch (InstantiationException | IllegalAccessException e) {
          LOGGER.error("Error instantiating plugin", e);
        }
      }
    }

    allDone = true;
  }

  private static void postInstall(Class<?> clazz) {
    Method postInstall;
    try {
      postInstall = clazz.getDeclaredMethod("postInstall", null);

      if (Modifier.isStatic(postInstall.getModifiers())) {
        postInstall.invoke(null, null);
      }
    }

    // Ignore all errors
    catch (SecurityException
        | NoSuchMethodException
        | IllegalArgumentException
        | IllegalAccessException
        | InvocationTargetException e) {
    }
  }

  private static void doUpdate(JLabel update, String text) {
    if (update == null) {
      return;
    }

    update.setText(text);
  }

  public static void instantiateDownloaded(JLabel update) {
    // These are found in the downloadedListenerClasses list
    for (Class<?> clazz : downloadedListenerClasses) {
      ExternalListener instance;

      try {
        doUpdate(update, Messages.getString("NetworkTab.48") + " " + clazz.getSimpleName());
        postInstall(clazz);
        LOGGER.debug("do inst of " + clazz.getSimpleName());
        instance = (ExternalListener) clazz.newInstance();
        doUpdate(update, instance.name() + " " + Messages.getString("NetworkTab.49"));
        registerListener(instance);

        if (PMS.get().getFrame() instanceof LooksFrame) {
          LooksFrame frame = (LooksFrame) PMS.get().getFrame();

          if (!frame.getPt().appendPlugin(instance)) {
            LOGGER.warn("Plugin limit of 30 has been reached");
          }
        }
      } catch (InstantiationException | IllegalAccessException e) {
        LOGGER.error("Error instantiating plugin", e);
      }
    }

    downloadedListenerClasses.clear();
  }

  public static boolean localPluginsInstalled() {
    return allDone;
  }

  private static boolean quoted(String s) {
    return s.startsWith("\"") && s.endsWith("\"");
  }

  private static String quote(String s) {
    if (quoted(s)) {
      return s;
    }
    return "\"" + s + "\"";
  }

  public static URLResult resolveURL(String url) {
    String quotedUrl = quote(url);
    for (URLResolver resolver : urlResolvers) {
      URLResult res = resolver.urlResolve(url);
      if (res != null) {
        if (StringUtils.isEmpty(res.url) || quotedUrl.equals(quote(res.url))) {
          res.url = null;
        }
        if (res.precoder != null && res.precoder.isEmpty()) {
          res.precoder = null;
        }
        if (res.args != null && res.args.isEmpty()) {
          res.args = null;
        }
        if (res.url != null || res.precoder != null || res.args != null) {
          LOGGER.debug(
              ((ExternalListener) resolver).name()
                  + " resolver:"
                  + (res.url == null ? "" : " url=" + res.url)
                  + (res.precoder == null ? "" : " precoder=" + res.precoder)
                  + (res.args == null ? "" : " args=" + res.args));
          return res;
        }
      }
    }
    return null;
  }

  public static void addURLResolver(URLResolver res) {
    if (urlResolvers.contains(res)) {
      return;
    }
    if (urlResolvers.isEmpty()) {
      urlResolvers.add(res);
      return;
    }

    String[] tmp = PMS.getConfiguration().getURLResolveOrder();
    if (tmp.length == 0) {
      // no order at all, just add it
      urlResolvers.add(res);
      return;
    }
    int id = -1;
    for (int i = 0; i < tmp.length; i++) {
      if (tmp[i].equalsIgnoreCase(res.name())) {
        id = i;
        break;
      }
    }

    if (id == -1) {
      // no order here, just add it
      urlResolvers.add(res);
      return;
    }
    if (id > urlResolvers.size()) {
      // add it last
      urlResolvers.add(res);
      return;
    }
    urlResolvers.add(id, res);
  }
}
public class SubtitleUtils {
  private static final Logger logger = LoggerFactory.getLogger(SubtitleUtils.class);
  private static PmsConfiguration configuration = PMS.getConfiguration();
  private static final Map<String, String> fileCharsetToMencoderSubcpOptionMap =
      new HashMap<String, String>() {
        {
          // Cyrillic / Russian
          put(CHARSET_IBM855, "enca:ru:cp1251");
          put(CHARSET_ISO_8859_5, "enca:ru:cp1251");
          put(CHARSET_KOI8_R, "enca:ru:cp1251");
          put(CHARSET_MACCYRILLIC, "enca:ru:cp1251");
          put(CHARSET_WINDOWS_1251, "enca:ru:cp1251");
          put(CHARSET_IBM866, "enca:ru:cp1251");
          // Greek
          put(CHARSET_WINDOWS_1253, "cp1253");
          put(CHARSET_ISO_8859_7, "ISO-8859-7");
          // Western Europe
          put(CHARSET_WINDOWS_1252, "cp1252");
          // Hebrew
          put(CHARSET_WINDOWS_1255, "cp1255");
          put(CHARSET_ISO_8859_8, "ISO-8859-8");
          // Chinese
          put(CHARSET_ISO_2022_CN, "ISO-2022-CN");
          put(CHARSET_BIG5, "enca:zh:big5");
          put(CHARSET_GB18030, "enca:zh:big5");
          put(CHARSET_EUC_TW, "enca:zh:big5");
          put(CHARSET_HZ_GB_2312, "enca:zh:big5");
          // Korean
          put(CHARSET_ISO_2022_KR, "cp949");
          put(CHARSET_EUC_KR, "euc-kr");
          // Japanese
          put(CHARSET_ISO_2022_JP, "ISO-2022-JP");
          put(CHARSET_EUC_JP, "euc-jp");
          put(CHARSET_SHIFT_JIS, "shift-jis");
        }
      };
  private static final EnumSet<SubtitleType> SUPPORTS_TIME_SHIFTING =
      EnumSet.of(SubtitleType.SUBRIP, SubtitleType.ASS);
  private static final DecimalFormat ASS_DECIMAL_FORMAT = new DecimalFormat("00.00");
  private static final DecimalFormat SRT_DECIMAL_FORMAT = new DecimalFormat("00.000");

  static {
    final DecimalFormatSymbols dotDecimalSeparator = new DecimalFormatSymbols();
    dotDecimalSeparator.setDecimalSeparator('.');
    ASS_DECIMAL_FORMAT.setDecimalFormatSymbols(dotDecimalSeparator);

    final DecimalFormatSymbols commaDecimalSeparator = new DecimalFormatSymbols();
    commaDecimalSeparator.setDecimalSeparator(',');
    SRT_DECIMAL_FORMAT.setDecimalFormatSymbols(commaDecimalSeparator);
  }

  /**
   * Returns value for -subcp option for non UTF-8 external subtitles based on detected charset.
   *
   * @param dlnaMediaSubtitle DLNAMediaSubtitle with external subtitles file.
   * @return value for mencoder's -subcp option or null if can't determine.
   */
  public static String getSubCpOptionForMencoder(DLNAMediaSubtitle dlnaMediaSubtitle) {
    if (dlnaMediaSubtitle == null) {
      throw new NullPointerException("dlnaMediaSubtitle can't be null.");
    }
    if (isBlank(dlnaMediaSubtitle.getExternalFileCharacterSet())) {
      return null;
    }
    return fileCharsetToMencoderSubcpOptionMap.get(dlnaMediaSubtitle.getExternalFileCharacterSet());
  }

  /**
   * Shift timing of subtitles in SSA/ASS or SRT format and converts charset to UTF8 if necessary
   *
   * @param inputSubtitles Subtitles file in SSA/ASS or SRT format
   * @param timeShift Time stamp value
   * @return Converted subtitles file
   * @throws IOException
   */
  public static DLNAMediaSubtitle shiftSubtitlesTimingWithUtfConversion(
      final DLNAMediaSubtitle inputSubtitles, double timeShift) throws IOException {
    if (inputSubtitles == null) {
      throw new NullPointerException("inputSubtitles should not be null.");
    }
    if (!inputSubtitles.isExternal()) {
      throw new IllegalArgumentException("inputSubtitles should be external.");
    }
    if (isBlank(inputSubtitles.getExternalFile().getName())) {
      throw new IllegalArgumentException(
          "inputSubtitles' external file should not have blank name.");
    }
    if (inputSubtitles.getType() == null) {
      throw new NullPointerException("inputSubtitles.getType() should not be null.");
    }
    if (!isSupportsTimeShifting(inputSubtitles.getType())) {
      throw new IllegalArgumentException(
          "inputSubtitles.getType() " + inputSubtitles.getType() + " is not supported.");
    }

    final File convertedSubtitlesFile =
        new File(
            configuration.getTempFolder(),
            getBaseName(inputSubtitles.getExternalFile().getName())
                + System.currentTimeMillis()
                + ".tmp");
    FileUtils.forceDeleteOnExit(convertedSubtitlesFile);
    BufferedReader input;

    final boolean isSubtitlesCodepageForcedInConfigurationAndSupportedByJVM =
        isNotBlank(configuration.getSubtitlesCodepage())
            && Charset.isSupported(configuration.getSubtitlesCodepage());
    final boolean isSubtitlesCodepageAutoDetectedAndSupportedByJVM =
        isNotBlank(inputSubtitles.getExternalFileCharacterSet())
            && Charset.isSupported(inputSubtitles.getExternalFileCharacterSet());
    if (isSubtitlesCodepageForcedInConfigurationAndSupportedByJVM) {
      input =
          new BufferedReader(
              new InputStreamReader(
                  new FileInputStream(inputSubtitles.getExternalFile()),
                  Charset.forName(configuration.getSubtitlesCodepage())));
    } else if (isSubtitlesCodepageAutoDetectedAndSupportedByJVM) {
      input =
          new BufferedReader(
              new InputStreamReader(
                  new FileInputStream(inputSubtitles.getExternalFile()),
                  Charset.forName(inputSubtitles.getExternalFileCharacterSet())));
    } else {
      input =
          new BufferedReader(
              new InputStreamReader(new FileInputStream(inputSubtitles.getExternalFile())));
    }
    final BufferedWriter output =
        new BufferedWriter(
            new OutputStreamWriter(
                new FileOutputStream(convertedSubtitlesFile), Charset.forName("UTF-8")));
    String line;
    double startTime;
    double endTime;

    try {
      if (SubtitleType.ASS.equals(inputSubtitles.getType())) {
        while ((line = input.readLine()) != null) {
          if (startsWith(line, "Dialogue:")) {
            String[] timings = splitPreserveAllTokens(line, ",");
            if (timings.length >= 3 && isNotBlank(timings[1]) && isNotBlank(timings[1])) {
              startTime = convertSubtitleTimingStringToTime(timings[1]);
              endTime = convertSubtitleTimingStringToTime(timings[2]);
              if (startTime >= timeShift) {
                timings[1] =
                    convertTimeToSubtitleTimingString(
                        startTime - timeShift, TimingFormat.ASS_TIMING);
                timings[2] =
                    convertTimeToSubtitleTimingString(endTime - timeShift, TimingFormat.ASS_TIMING);
                output.write(join(timings, ",") + "\n");
              } else {
                continue;
              }
            } else {
              output.write(line + "\n");
            }
          } else {
            output.write(line + "\n");
          }
        }
      } else if (SubtitleType.SUBRIP.equals(inputSubtitles.getType())) {
        int n = 1;
        while ((line = input.readLine()) != null) {
          if (contains(line, ("-->"))) {
            startTime =
                convertSubtitleTimingStringToTime(line.substring(0, line.indexOf("-->") - 1));
            endTime = convertSubtitleTimingStringToTime(line.substring(line.indexOf("-->") + 4));
            if (startTime >= timeShift) {
              output.write("" + (n++) + "\n");
              output.write(
                  convertTimeToSubtitleTimingString(
                      startTime - timeShift, TimingFormat.SRT_TIMING));
              output.write(" --> ");
              output.write(
                  convertTimeToSubtitleTimingString(endTime - timeShift, TimingFormat.SRT_TIMING)
                      + "\n");

              while (isNotBlank(line = input.readLine())) { // Read all following subs lines
                output.write(line + "\n");
              }
              output.write("" + "\n");
            }
          }
        }
      }
    } finally {
      if (output != null) {
        output.flush();
        output.close();
      }
      if (input != null) {
        input.close();
      }
    }

    final DLNAMediaSubtitle convertedSubtitles = new DLNAMediaSubtitle();
    convertedSubtitles.setExternalFile(convertedSubtitlesFile);
    convertedSubtitles.setType(inputSubtitles.getType());
    convertedSubtitles.setLang(inputSubtitles.getLang());
    convertedSubtitles.setFlavor(inputSubtitles.getFlavor());
    convertedSubtitles.setId(inputSubtitles.getId());
    return convertedSubtitles;
  }

  /**
   * Check if subtitleType supports time shifting
   *
   * @param subtitleType to check
   * @return true if subtitleType can be time shifted with {@link
   *     #shiftSubtitlesTimingWithUtfConversion(net.pms.dlna.DLNAMediaSubtitle, double)}
   */
  public static boolean isSupportsTimeShifting(SubtitleType subtitleType) {
    return SUPPORTS_TIME_SHIFTING.contains(subtitleType);
  }

  enum TimingFormat {
    ASS_TIMING,
    SRT_TIMING,
    SECONDS_TIMING;
  }

  /**
   * Converts time in seconds to subtitle timing string.
   *
   * @param time in seconds
   * @param timingFormat format of timing string
   * @return timing string
   */
  static String convertTimeToSubtitleTimingString(
      final double time, final TimingFormat timingFormat) {
    if (timingFormat == null) {
      throw new NullPointerException("timingFormat should not be null.");
    }

    double s = Math.abs(time % 60);
    int h = (int) (time / 3600);
    int m = Math.abs(((int) (time / 60)) % 60);
    switch (timingFormat) {
      case ASS_TIMING:
        return trim(String.format("% 02d:%02d:%s", h, m, ASS_DECIMAL_FORMAT.format(s)));
      case SRT_TIMING:
        return trim(String.format("% 03d:%02d:%s", h, m, SRT_DECIMAL_FORMAT.format(s)));
      case SECONDS_TIMING:
        return trim(String.format("% 03d:%02d:%02.0f", h, m, s));
      default:
        return trim(String.format("% 03d:%02d:%02.0f", h, m, s));
    }
  }

  /**
   * Converts subtitle timing string to seconds.
   *
   * @param timingString in format OO:00:00.000
   * @return seconds or null if conversion failed
   */
  static Double convertSubtitleTimingStringToTime(final String timingString)
      throws NumberFormatException {
    if (isBlank(timingString)) {
      throw new IllegalArgumentException("timingString should not be blank.");
    }

    final StringTokenizer st = new StringTokenizer(timingString, ":");
    try {
      int h = Integer.parseInt(st.nextToken());
      int m = Integer.parseInt(st.nextToken());
      double s = Double.parseDouble(replace(st.nextToken(), ",", "."));
      if (h >= 0) {
        return h * 3600 + m * 60 + s;
      } else {
        return h * 3600 - m * 60 - s;
      }
    } catch (NumberFormatException nfe) {
      logger.debug("Failed to convert timing string \"" + timingString + "\".");
      throw nfe;
    }
  }

  /**
   * For testing purposes.
   *
   * @param configuration
   */
  static void setConfiguration(PmsConfiguration configuration) {
    SubtitleUtils.configuration = configuration;
  }
}
public class PipeProcess {
  private static final Logger LOGGER = LoggerFactory.getLogger(PipeProcess.class);
  private static final PmsConfiguration configuration = PMS.getConfiguration();

  private String linuxPipeName;
  private WindowsNamedPipe mk;
  private boolean forcereconnect;

  public PipeProcess(String pipeName, OutputParams params, String... extras) {
    forcereconnect = false;
    boolean in = true;

    if (extras != null && extras.length > 0 && extras[0].equals("out")) {
      in = false;
    }

    if (extras != null) {
      for (int i = 0; i < extras.length; i++) {
        if (extras[i].equals("reconnect")) {
          forcereconnect = true;
        }
      }
    }

    if (PMS.get().isWindows()) {
      mk = new WindowsNamedPipe(pipeName, forcereconnect, in, params);
    } else {
      linuxPipeName = getPipeName(pipeName);
    }
  }

  public PipeProcess(String pipeName, String... extras) {
    this(pipeName, null, extras);
  }

  private static String getPipeName(String pipeName) {
    try {
      return configuration.getTempFolder() + "/" + pipeName;
    } catch (IOException e) {
      LOGGER.error("Pipe may not be in temporary directory", e);
      return pipeName;
    }
  }

  public String getInputPipe() {
    if (!PMS.get().isWindows()) {
      return linuxPipeName;
    }
    return mk.getPipeName();
  }

  public String getOutputPipe() {
    if (!PMS.get().isWindows()) {
      return linuxPipeName;
    }
    return mk.getPipeName();
  }

  public ProcessWrapper getPipeProcess() {
    if (!PMS.get().isWindows()) {
      OutputParams mkfifo_vid_params = new OutputParams(configuration);
      mkfifo_vid_params.maxBufferSize = 0.1;
      mkfifo_vid_params.log = true;
      String cmdArray[];

      if (Platform.isMac() || Platform.isFreeBSD() || Platform.isSolaris()) {
        cmdArray = new String[] {"mkfifo", "-m", "777", linuxPipeName};
      } else {
        cmdArray = new String[] {"mkfifo", "--mode=777", linuxPipeName};
      }

      ProcessWrapperImpl mkfifo_vid_process = new ProcessWrapperImpl(cmdArray, mkfifo_vid_params);
      return mkfifo_vid_process;
    }

    return mk;
  }

  public void deleteLater() {
    if (!PMS.get().isWindows()) {
      File f = new File(linuxPipeName);
      f.deleteOnExit();
    }
  }

  public BufferedOutputFile getDirectBuffer() throws IOException {
    if (!PMS.get().isWindows()) {
      return null;
    }

    return mk.getDirectBuffer();
  }

  public InputStream getInputStream() throws IOException {
    if (!PMS.get().isWindows()) {
      LOGGER.trace("Opening file " + linuxPipeName + " for reading...");
      RandomAccessFile raf = new RandomAccessFile(linuxPipeName, "r");

      return new FileInputStream(raf.getFD());
    }

    return mk.getReadable();
  }

  public OutputStream getOutputStream() throws IOException {
    if (!PMS.get().isWindows()) {
      LOGGER.trace("Opening file " + linuxPipeName + " for writing...");
      RandomAccessFile raf = new RandomAccessFile(linuxPipeName, "rw");

      return new FileOutputStream(raf.getFD());
    }

    return mk.getWritable();
  }
}
public class Request extends HTTPResource {
  private static final Logger logger = LoggerFactory.getLogger(Request.class);
  private static final PmsConfiguration configuration = PMS.getConfiguration();

  private static final String CRLF = "\r\n";
  private static final String HTTP_200_OK = "HTTP/1.1 200 OK";
  private static final String HTTP_500 = "HTTP/1.1 500 Internal Server Error";
  private static final String HTTP_206_OK = "HTTP/1.1 206 Partial Content";
  private static final String HTTP_200_OK_10 = "HTTP/1.0 200 OK";
  private static final String HTTP_206_OK_10 = "HTTP/1.0 206 Partial Content";
  private static final String CONTENT_TYPE_UTF8 = "CONTENT-TYPE: text/xml; charset=\"utf-8\"";
  private static final String CONTENT_TYPE = "Content-Type: text/xml; charset=\"utf-8\"";
  private static SimpleDateFormat sdf =
      new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US);
  private final String method;
  private String argument;
  private String soapaction;
  private String content;
  private OutputStream output;
  private String objectID;
  private int startingIndex;
  private int requestCount;
  private String browseFlag;
  private long lowRange;
  private InputStream inputStream;
  private RendererConfiguration mediaRenderer;
  private String transferMode;
  private String contentFeatures;
  private double timeseek;
  private double timeRangeEnd;
  private long highRange;
  private boolean http10;

  public RendererConfiguration getMediaRenderer() {
    return mediaRenderer;
  }

  public void setMediaRenderer(RendererConfiguration mediaRenderer) {
    this.mediaRenderer = mediaRenderer;
  }

  public InputStream getInputStream() {
    return inputStream;
  }

  public long getLowRange() {
    return lowRange;
  }

  public void setLowRange(long lowRange) {
    this.lowRange = lowRange;
  }

  public String getTransferMode() {
    return transferMode;
  }

  public void setTransferMode(String transferMode) {
    this.transferMode = transferMode;
  }

  public String getContentFeatures() {
    return contentFeatures;
  }

  public void setContentFeatures(String contentFeatures) {
    this.contentFeatures = contentFeatures;
  }

  public double getTimeseek() {
    return timeseek;
  }

  public void setTimeseek(double timeseek) {
    this.timeseek = timeseek;
  }

  public void setTimeRangeEnd(double timeRangeEnd) {
    this.timeRangeEnd = timeRangeEnd;
  }

  public long getHighRange() {
    return highRange;
  }

  public void setHighRange(long highRange) {
    this.highRange = highRange;
  }

  public boolean isHttp10() {
    return http10;
  }

  public void setHttp10(boolean http10) {
    this.http10 = http10;
  }

  public Request(String method, String argument) {
    this.method = method;
    this.argument = argument;
  }

  public String getSoapaction() {
    return soapaction;
  }

  public void setSoapaction(String soapaction) {
    this.soapaction = soapaction;
  }

  public String getTextContent() {
    return content;
  }

  public void setTextContent(String content) {
    this.content = content;
  }

  public String getMethod() {
    return method;
  }

  public String getArgument() {
    return argument;
  }

  public void answer(OutputStream output, StartStopListenerDelegate startStopListenerDelegate)
      throws IOException {
    this.output = output;

    long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit
    if (lowRange != 0 || highRange != 0) {
      output(output, http10 ? HTTP_206_OK_10 : HTTP_206_OK);
    } else {
      if (soapaction != null && soapaction.contains("X_GetFeatureList")) {
        //  If we don't return a 500 error, Samsung 2012 TVs time out.
        output(output, HTTP_500);
      } else {
        output(output, http10 ? HTTP_200_OK_10 : HTTP_200_OK);
      }
    }

    StringBuilder response = new StringBuilder();
    DLNAResource dlna = null;
    boolean xbox = mediaRenderer.isXBOX();

    // Samsung 2012 TVs have a problematic preceding slash that needs to be removed.
    if (argument.startsWith("/")) {
      logger.trace("Stripping preceding slash from: " + argument);
      argument = argument.substring(1);
    }

    if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) {
      output(output, "Content-Type: text/html");
      response.append(HTMLConsole.servePage(argument.substring(8)));
    } else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) {
      String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/"));
      id = id.replace("%24", "$"); // popcorn hour ?
      List<DLNAResource> files =
          PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer);
      if (transferMode != null) {
        output(output, "TransferMode.DLNA.ORG: " + transferMode);
      }
      if (files.size() == 1) {
        // DNLAresource was found.
        dlna = files.get(0);
        String fileName = argument.substring(argument.lastIndexOf("/") + 1);

        if (fileName.startsWith("thumbnail0000")) {
          // This is a request for a thumbnail file.
          output(output, "Content-Type: " + dlna.getThumbnailContentType());
          output(output, "Accept-Ranges: bytes");
          output(output, "Expires: " + getFUTUREDATE() + " GMT");
          output(output, "Connection: keep-alive");
          if (mediaRenderer.isMediaParserV2()) {
            dlna.checkThumbnail();
          }
          inputStream = dlna.getThumbnailInputStream();
        } else if (fileName.indexOf("subtitle0000") > -1) {
          // This is a request for a subtitle file
          output(output, "Content-Type: text/plain");
          output(output, "Expires: " + getFUTUREDATE() + " GMT");
          List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitleTracksList();

          if (subs != null && !subs.isEmpty()) {
            // TODO: maybe loop subs to get the requested subtitle type instead of using the first
            // one
            DLNAMediaSubtitle sub = subs.get(0);
            if (sub.isExternal()) {
              inputStream = new java.io.FileInputStream(sub.getExternalFile());
            }
          }
        } else {
          // This is a request for a regular file.
          String name = dlna.getDisplayName(mediaRenderer);
          inputStream =
              dlna.getInputStream(
                  Range.create(lowRange, highRange, timeseek, timeRangeEnd), mediaRenderer);
          if (inputStream == null) {
            // No inputStream indicates that transcoding / remuxing probably crashed.
            logger.error("There is no inputstream to return for " + name);
          } else {
            startStopListenerDelegate.start(dlna);
            output(output, "Content-Type: " + getRendererMimeType(dlna.mimeType(), mediaRenderer));

            if (!configuration.isDisableSubtitles()) {
              // Some renderers (like Samsung devices) allow a custom header for a subtitle URL
              String subtitleHttpHeader = mediaRenderer.getSubtitleHttpHeader();

              if (subtitleHttpHeader != null && !"".equals(subtitleHttpHeader)) {
                // Device allows a custom subtitle HTTP header; construct it
                List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitleTracksList();

                if (subs != null && !subs.isEmpty()) {
                  DLNAMediaSubtitle sub = subs.get(0);
                  String subtitleUrl;
                  String subExtension = sub.getType().getExtension();

                  if (isNotBlank(subExtension)) {
                    subtitleUrl =
                        "http://"
                            + PMS.get().getServer().getHost()
                            + ':'
                            + PMS.get().getServer().getPort()
                            + "/get/"
                            + id
                            + "/subtitle0000."
                            + subExtension;
                  } else {
                    subtitleUrl =
                        "http://"
                            + PMS.get().getServer().getHost()
                            + ':'
                            + PMS.get().getServer().getPort()
                            + "/get/"
                            + id
                            + "/subtitle0000";
                  }

                  output(output, subtitleHttpHeader + ": " + subtitleUrl);
                }
              }
            }

            final DLNAMediaInfo media = dlna.getMedia();

            if (media != null) {
              if (StringUtils.isNotBlank(media.getContainer())) {
                name += " [container: " + media.getContainer() + "]";
              }

              if (StringUtils.isNotBlank(media.getCodecV())) {
                name += " [video: " + media.getCodecV() + "]";
              }
            }

            PMS.get().getFrame().setStatusLine("Serving " + name);

            // Response generation:
            // We use -1 for arithmetic convenience but don't send it as a value.
            // If Content-Length < 0 we omit it, for Content-Range we use '*' to signify
            // unspecified.

            boolean chunked = mediaRenderer.isChunkedTransfer();

            // Determine the total size. Note: when transcoding the length is
            // not known in advance, so DLNAMediaInfo.TRANS_SIZE will be returned instead.

            long totalsize = dlna.length(mediaRenderer);

            if (chunked && totalsize == DLNAMediaInfo.TRANS_SIZE) {
              // In chunked mode we try to avoid arbitrary values.
              totalsize = -1;
            }

            long remaining = totalsize - lowRange;
            long requested = highRange - lowRange;

            if (requested != 0) {
              // Determine the range (i.e. smaller of known or requested bytes)
              long bytes = remaining > -1 ? remaining : inputStream.available();

              if (requested > 0 && bytes > requested) {
                bytes = requested + 1;
              }

              // Calculate the corresponding highRange (this is usually redundant).
              highRange = lowRange + bytes - (bytes > 0 ? 1 : 0);

              logger.trace(
                  (chunked ? "Using chunked response. " : "") + "Sending " + bytes + " bytes.");

              output(
                  output,
                  "Content-Range: bytes "
                      + lowRange
                      + "-"
                      + (highRange > -1 ? highRange : "*")
                      + "/"
                      + (totalsize > -1 ? totalsize : "*"));

              // Content-Length refers to the current chunk size here, though in chunked
              // mode if the request is open-ended and totalsize is unknown we omit it.
              if (chunked && requested < 0 && totalsize < 0) {
                CLoverride = -1;
              } else {
                CLoverride = bytes;
              }
            } else {
              // Content-Length refers to the total remaining size of the stream here.
              CLoverride = remaining;
            }

            if (contentFeatures != null) {
              output(output, "ContentFeatures.DLNA.ORG: " + dlna.getDlnaContentFeatures());
            }

            if (dlna.getPlayer() == null || xbox) {
              output(output, "Accept-Ranges: bytes");
            }

            output(output, "Connection: keep-alive");
          }
        }
      }
    } else if ((method.equals("GET") || method.equals("HEAD"))
        && (argument.toLowerCase().endsWith(".png")
            || argument.toLowerCase().endsWith(".jpg")
            || argument.toLowerCase().endsWith(".jpeg"))) {
      if (argument.toLowerCase().endsWith(".png")) {
        output(output, "Content-Type: image/png");
      } else {
        output(output, "Content-Type: image/jpeg");
      }
      output(output, "Accept-Ranges: bytes");
      output(output, "Connection: keep-alive");
      output(output, "Expires: " + getFUTUREDATE() + " GMT");
      inputStream = getResourceInputStream(argument);
    } else if ((method.equals("GET") || method.equals("HEAD"))
        && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) {
      String profileName = configuration.getProfileName();
      output(output, CONTENT_TYPE);
      output(output, "Cache-Control: no-cache");
      output(output, "Expires: 0");
      output(output, "Accept-Ranges: bytes");
      output(output, "Connection: keep-alive");
      inputStream =
          getResourceInputStream((argument.equals("description/fetch") ? "PMS.xml" : argument));

      if (argument.equals("description/fetch")) {
        byte b[] = new byte[inputStream.available()];
        inputStream.read(b);
        String s = new String(b);
        s = s.replace("[uuid]", PMS.get().usn()); // .substring(0, PMS.get().usn().length()-2));
        s = s.replace("[host]", PMS.get().getServer().getHost());
        s = s.replace("[port]", "" + PMS.get().getServer().getPort());
        if (xbox) {
          logger.debug("DLNA changes for Xbox 360");
          s =
              s.replace(
                  "PS3 Media Server",
                  "PS3 Media Server [" + profileName + "] : Windows Media Connect");
          s =
              s.replace(
                  "<modelName>PMS</modelName>", "<modelName>Windows Media Connect</modelName>");
          s =
              s.replace(
                  "<serviceList>",
                  "<serviceList>"
                      + CRLF
                      + "<service>"
                      + CRLF
                      + "<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>"
                      + CRLF
                      + "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>"
                      + CRLF
                      + "<SCPDURL>/upnp/mrr/scpd</SCPDURL>"
                      + CRLF
                      + "<controlURL>/upnp/mrr/control</controlURL>"
                      + CRLF
                      + "</service>"
                      + CRLF);

        } else {
          s = s.replace("PS3 Media Server", "PS3 Media Server [" + profileName + "]");
        }
        inputStream = new ByteArrayInputStream(s.getBytes());
      }
    } else if (method.equals("POST")
        && (argument.contains("MS_MediaReceiverRegistrar_control")
            || argument.contains("mrr/control"))) {
      output(output, CONTENT_TYPE_UTF8);
      response.append(HTTPXMLHelper.XML_HEADER);
      response.append(CRLF);
      response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
      response.append(CRLF);
      if (soapaction != null && soapaction.contains("IsAuthorized")) {
        response.append(HTTPXMLHelper.XBOX_2);
        response.append(CRLF);
      } else if (soapaction != null && soapaction.contains("IsValidated")) {
        response.append(HTTPXMLHelper.XBOX_1);
        response.append(CRLF);
      }
      response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
      response.append(CRLF);
    } else if (method.equals("POST") && argument.endsWith("upnp/control/connection_manager")) {
      output(output, CONTENT_TYPE_UTF8);
      if (soapaction != null && soapaction.indexOf("ConnectionManager:1#GetProtocolInfo") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.PROTOCOLINFO_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      }
    } else if (method.equals("SUBSCRIBE")) {
      if (soapaction == null) {
        // Ignore this
        return;
      }
      output(output, CONTENT_TYPE_UTF8);
      output(output, "Content-Length: 0");
      output(output, "Connection: close");
      output(output, "SID: " + PMS.get().usn());
      output(output, "Server: " + PMS.get().getServerName());
      output(output, "Timeout: Second-1800");
      output(output, "");
      output.flush();
      // output.close();

      String cb = soapaction.replace("<", "").replace(">", "");

      try {
        URL soapActionUrl = new URL(cb);
        String addr = soapActionUrl.getHost();
        int port = soapActionUrl.getPort();
        Socket sock = new Socket(addr, port);
        OutputStream out = sock.getOutputStream();

        output(out, "NOTIFY /" + argument + " HTTP/1.1");
        output(out, "SID: " + PMS.get().usn());
        output(out, "SEQ: " + 0);
        output(out, "NT: upnp:event");
        output(out, "NTS: upnp:propchange");
        output(out, "HOST: " + addr + ":" + port);
        output(out, CONTENT_TYPE_UTF8);
        sock.close();
      } catch (MalformedURLException ex) {
        logger.debug("Cannot parse address and port from soap action \"" + soapaction + "\"", ex);
      }

      if (argument.contains("connection_manager")) {
        response.append(
            HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ConnectionManager:1"));
        response.append(HTTPXMLHelper.eventProp("SinkProtocolInfo"));
        response.append(HTTPXMLHelper.eventProp("SourceProtocolInfo"));
        response.append(HTTPXMLHelper.eventProp("CurrentConnectionIDs"));
        response.append(HTTPXMLHelper.EVENT_FOOTER);
      } else if (argument.contains("content_directory")) {
        response.append(
            HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ContentDirectory:1"));
        response.append(HTTPXMLHelper.eventProp("TransferIDs"));
        response.append(HTTPXMLHelper.eventProp("ContainerUpdateIDs"));
        response.append(
            HTTPXMLHelper.eventProp("SystemUpdateID", "" + DLNAResource.getSystemUpdateId()));
        response.append(HTTPXMLHelper.EVENT_FOOTER);
      }
    } else if (method.equals("POST") && argument.endsWith("upnp/control/content_directory")) {
      output(output, CONTENT_TYPE_UTF8);

      if (soapaction != null && soapaction.indexOf("ContentDirectory:1#GetSystemUpdateID") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_HEADER);
        response.append(CRLF);
        response.append("<Id>").append(DLNAResource.getSystemUpdateId()).append("</Id>");
        response.append(CRLF);
        response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction != null
          && soapaction.indexOf("ContentDirectory:1#GetSortCapabilities") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SORTCAPS_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction != null
          && soapaction.indexOf("ContentDirectory:1#X_GetFeatureList")
              > -1) { // Added for Samsung 2012 TVs
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.UPNP_INVALID_ACTION);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction != null
          && soapaction.indexOf("ContentDirectory:1#GetSearchCapabilities") > -1) {
        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SEARCHCAPS_RESPONSE);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
      } else if (soapaction != null
          && (soapaction.contains("ContentDirectory:1#Browse")
              || soapaction.contains("ContentDirectory:1#Search"))) {
        objectID = getEnclosingValue(content, "<ObjectID>", "</ObjectID>");
        String containerID = null;
        if (isEmpty(objectID) && xbox) {
          containerID = getEnclosingValue(content, "<ContainerID>", "</ContainerID>");
          if (containerID == null || !containerID.contains("$")) {
            objectID = "0";
          } else {
            objectID = containerID;
            containerID = null;
          }
        }
        Object sI = getEnclosingValue(content, "<StartingIndex>", "</StartingIndex>");
        Object rC = getEnclosingValue(content, "<RequestedCount>", "</RequestedCount>");
        browseFlag = getEnclosingValue(content, "<BrowseFlag>", "</BrowseFlag>");
        if (sI != null) {
          startingIndex = Integer.parseInt(sI.toString());
        }
        if (rC != null) {
          requestCount = Integer.parseInt(rC.toString());
        }

        response.append(HTTPXMLHelper.XML_HEADER);
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER);
        response.append(CRLF);
        if (soapaction != null && soapaction.contains("ContentDirectory:1#Search")) {
          response.append(HTTPXMLHelper.SEARCHRESPONSE_HEADER);
        } else {
          response.append(HTTPXMLHelper.BROWSERESPONSE_HEADER);
        }
        response.append(CRLF);
        response.append(HTTPXMLHelper.RESULT_HEADER);

        response.append(HTTPXMLHelper.DIDL_HEADER);

        if (soapaction != null && soapaction.contains("ContentDirectory:1#Search")) {
          browseFlag = "BrowseDirectChildren";
        }

        // XBOX virtual containers ... doh
        String searchCriteria = null;
        if (xbox
            && configuration.getUseCache()
            && PMS.get().getLibrary() != null
            && containerID != null) {
          if (containerID.equals("7") && PMS.get().getLibrary().getAlbumFolder() != null) {
            objectID = PMS.get().getLibrary().getAlbumFolder().getResourceId();
          } else if (containerID.equals("6") && PMS.get().getLibrary().getArtistFolder() != null) {
            objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
          } else if (containerID.equals("5") && PMS.get().getLibrary().getGenreFolder() != null) {
            objectID = PMS.get().getLibrary().getGenreFolder().getResourceId();
          } else if (containerID.equals("F")
              && PMS.get().getLibrary().getPlaylistFolder() != null) {
            objectID = PMS.get().getLibrary().getPlaylistFolder().getResourceId();
          } else if (containerID.equals("4") && PMS.get().getLibrary().getAllFolder() != null) {
            objectID = PMS.get().getLibrary().getAllFolder().getResourceId();
          } else if (containerID.equals("1")) {
            String artist = getEnclosingValue(content, "upnp:artist = &quot;", "&quot;)");
            if (artist != null) {
              objectID = PMS.get().getLibrary().getArtistFolder().getResourceId();
              searchCriteria = artist;
            }
          }
        }

        List<DLNAResource> files =
            PMS.get()
                .getRootFolder(mediaRenderer)
                .getDLNAResources(
                    objectID,
                    browseFlag != null && browseFlag.equals("BrowseDirectChildren"),
                    startingIndex,
                    requestCount,
                    mediaRenderer);

        if (searchCriteria != null && files != null) {
          for (int i = files.size() - 1; i >= 0; i--) {
            if (!files.get(i).getName().equals(searchCriteria)) {
              files.remove(i);
            }
          }

          if (files.size() > 0) {
            files = files.get(0).getChildren();
          }
        }

        int minus = 0;
        if (files != null) {
          for (DLNAResource uf : files) {
            if (xbox && containerID != null) {
              uf.setFakeParentId(containerID);
            }

            if (uf.isCompatible(mediaRenderer)
                && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer))) {
              response.append(uf.getDidlString(mediaRenderer));
            } else {
              minus++;
            }
          }
        }

        response.append(HTTPXMLHelper.DIDL_FOOTER);
        response.append(HTTPXMLHelper.RESULT_FOOTER);
        response.append(CRLF);

        int filessize = 0;
        if (files != null) {
          filessize = files.size();
        }

        response.append("<NumberReturned>").append(filessize - minus).append("</NumberReturned>");
        response.append(CRLF);
        DLNAResource parentFolder = null;

        if (files != null && filessize > 0) {
          parentFolder = files.get(0).getParent();
        }

        if (browseFlag != null
            && browseFlag.equals("BrowseDirectChildren")
            && mediaRenderer.isMediaParserV2()
            && mediaRenderer.isDLNATreeHack()) {
          // with the new parser, files are parsed and analyzed *before*
          // creating the DLNA tree, every 10 items (the ps3 asks 10 by 10),
          // so we do not know exactly the total number of items in the DLNA folder to send
          // (regular files, plus the #transcode folder, maybe the #imdb one, also files can be
          // invalidated and hidden if format is broken or encrypted, etc.).
          // let's send a fake total size to force the renderer to ask following items
          int totalCount = startingIndex + requestCount + 1; // returns 11 when 10 asked

          if (filessize - minus <= 0) { // if no more elements, send startingIndex
            totalCount = startingIndex;
          }

          response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>");
        } else if (browseFlag != null && browseFlag.equals("BrowseDirectChildren")) {
          response
              .append("<TotalMatches>")
              .append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus)
              .append("</TotalMatches>");
        } else {
          // from upnp spec: If BrowseMetadata is specified in the BrowseFlags then TotalMatches = 1
          response.append("<TotalMatches>1</TotalMatches>");
        }

        response.append(CRLF);
        response.append("<UpdateID>");

        if (parentFolder != null) {
          response.append(parentFolder.getUpdateId());
        } else {
          response.append("1");
        }

        response.append("</UpdateID>");
        response.append(CRLF);
        if (soapaction != null && soapaction.contains("ContentDirectory:1#Search")) {
          response.append(HTTPXMLHelper.SEARCHRESPONSE_FOOTER);
        } else {
          response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER);
        }
        response.append(CRLF);
        response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER);
        response.append(CRLF);
        // logger.trace(response.toString());
      }
    }

    output(output, "Server: " + PMS.get().getServerName());

    if (response.length() > 0) {
      byte responseData[] = response.toString().getBytes("UTF-8");
      output(output, "Content-Length: " + responseData.length);
      output(output, "");

      if (!method.equals("HEAD")) {
        output.write(responseData);
        // logger.trace(response.toString());
      }
    } else if (inputStream != null) {
      if (CLoverride > -2) {
        // Content-Length override has been set, send or omit as appropriate
        if (CLoverride > -1 && CLoverride != DLNAMediaInfo.TRANS_SIZE) {
          // Since PS3 firmware 2.50, it is wiser not to send an arbitrary Content-Length,
          // as the PS3 will display a network error and request the last seconds of the
          // transcoded video. Better to send no Content-Length at all.
          output(output, "Content-Length: " + CLoverride);
        }
      } else {
        int cl = inputStream.available();
        logger.trace("Available Content-Length: " + cl);
        output(output, "Content-Length: " + cl);
      }

      if (timeseek > 0 && dlna != null) {
        String timeseekValue = DLNAMediaInfo.getDurationString(timeseek);
        String timetotalValue = dlna.getMedia().getDurationString();
        output(
            output,
            "TimeSeekRange.dlna.org: npt="
                + timeseekValue
                + "-"
                + timetotalValue
                + "/"
                + timetotalValue);
        output(
            output,
            "X-Seek-Range: npt=" + timeseekValue + "-" + timetotalValue + "/" + timetotalValue);
      }

      output(output, "");
      int sendB = 0;

      if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) {
        sendB =
            sendBytes(inputStream); // , ((lowRange > 0 && highRange > 0)?(highRange-lowRange):-1)
      }

      logger.trace("Sending stream: " + sendB + " bytes of " + argument);
      PMS.get().getFrame().setStatusLine(null);
    } else { // inputStream is null
      if (lowRange > 0 && highRange > 0) {
        output(output, "Content-Length: " + (highRange - lowRange + 1));
      } else {
        output(output, "Content-Length: 0");
      }

      output(output, "");
    }
  }

  private void output(OutputStream output, String line) throws IOException {
    output.write((line + CRLF).getBytes("UTF-8"));
    logger.trace("Wrote on socket: " + line);
  }

  private String getFUTUREDATE() {
    sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
    return sdf.format(new Date(10000000000L + System.currentTimeMillis()));
  }

  // VISTA tip ?: netsh interface tcp set global autotuninglevel=disabled
  private int sendBytes(InputStream fis) throws IOException {
    byte[] buffer = new byte[32 * 1024];
    int bytes;
    int sendBytes = 0;

    try {
      while ((bytes = fis.read(buffer)) != -1) {
        output.write(buffer, 0, bytes);
        sendBytes += bytes;
      }
    } catch (IOException e) {
      logger.trace(
          "Sending stream with premature end: "
              + sendBytes
              + " bytes of "
              + argument
              + ". Reason: "
              + e.getMessage());
    } finally {
      fis.close();
    }

    return sendBytes;
  }

  private String getEnclosingValue(String content, String leftTag, String rightTag) {
    String result = null;
    int leftTagPos = content.indexOf(leftTag);
    int rightTagPos = content.indexOf(rightTag, leftTagPos + 1);

    if (leftTagPos > -1 && rightTagPos > leftTagPos) {
      result = content.substring(leftTagPos + leftTag.length(), rightTagPos);
    }

    return result;
  }
}
/**
 * This is a plugin for ps3mediaserver @see http://code.google.com/p/ps3mediaserver/ It allow you to
 * keep track of which files were viewed.
 *
 * @author Cees-Willem Hofstede <*****@*****.**>
 */
public class ViewStatus implements StartStopListener, ThumbnailExtras, ActionListener {
  private static final Logger log = LoggerFactory.getLogger(ViewStatus.class);
  private boolean enabledMV;
  private JCheckBox cbEnableMV;
  Date previousDate;

  private Queue<Date> startDates = new LinkedList<Date>();

  PmsConfiguration PMSConf = PMS.getConfiguration();

  public ViewStatus() {
    if (PMSConf.getCustomProperty("enableViewStatus") == null) {
      // if not set in configuration, enable plugin by default
      PMSConf.setCustomProperty("enableViewStatus", true);
    }
    enabledMV =
        PMSConf.getCustomProperty("enableViewStatus").equals("true"); // true if plugin is enabled
  }

  @Override
  public void donePlaying(DLNAMediaInfo media, DLNAResource resource) {
    // currently only for videofiles
    if (enabledMV && resource.getType() == Format.VIDEO) {
      // get path information
      Path infoFilePath = Paths.get(resource.getSystemName());
      String folderName = infoFilePath.getParent().toString();
      String infoFile = folderName + "/.viewstatus";
      String infoKey = resource.getName();

      // create handler for properties
      Properties props = new Properties();

      double fileViewPercentage = 0;

      try {
        props.load(new FileInputStream(infoFile)); // load the viewinfo file (if any)
        fileViewPercentage = Integer.parseInt(props.getProperty(infoKey, "0"));
      } catch (IOException e) {
        log.error("viewinfo at " + infoFile + " file does not yet exist");
      }

      double playLengthSec = 0; // total length of the file

      /**
       * @TODO: calculation below should work without startdate. Is it possible to get the exact
       * number of seconds the file was stopped?
       */
      playLengthSec = (int) (new Date().getTime() - startDates.poll().getTime()) / 1000;

      double fullLengthSec = media.getDurationInSeconds();

      if (fullLengthSec > 0) {
        double currentFileViewPercentage = (playLengthSec / fullLengthSec) * 100;

        // if the watched percentage is bigger than in the viewinfo file, write it to viewinfo
        if (currentFileViewPercentage > fileViewPercentage) {
          fileViewPercentage = Math.min(100, currentFileViewPercentage);
          props.setProperty(infoKey, Integer.toString((int) fileViewPercentage));

          try {
            props.store(new FileOutputStream(infoFile), null);

            // update the thumbnail
            media.setThumb(null);
            InputFile input = new InputFile();
            input.setFile(((RealFile) resource).getFile());
            media.generateThumbnail(input, resource.getExt(), resource.getType());

          } catch (IOException e) {
            logExeptionError(e);
          }
        }
      }
    }
  }

  @Override
  public void nowPlaying(DLNAMediaInfo media, DLNAResource resource) {
    if (enabledMV && resource.getType() == Format.VIDEO) {
      startDates.add(new Date()); // set the startdate
    }
  }

  @Override
  public JComponent config() {
    JPanel configPanel = new JPanel();
    cbEnableMV = new JCheckBox("enable mark viewed"); // $NON-NLS-1$
    cbEnableMV.setSelected(enabledMV);
    cbEnableMV.addActionListener(this);
    configPanel.add(cbEnableMV);
    return configPanel;
  }

  @Override
  public void shutdown() {}

  @Override
  public String name() {
    return "View Status";
  }

  @Override
  public void updateThumb(DLNAMediaInfo media, InputFile f) {
    try {
      BufferedImage image = ImageIO.read(new ByteArrayInputStream(media.getThumb()));

      if (image != null) {
        Graphics g = image.getGraphics();
        Path infoFilePath = Paths.get(f.getFile().getPath()); // get path of current file
        String folderName = infoFilePath.getParent().toString(); // get folder
        String infoFile = folderName + "/.viewstatus"; // get get infofilename
        String infoKey = f.getFile().getName(); // get keyname

        Properties props = new Properties();

        try {
          props.load(new FileInputStream(infoFile));
          String viewInfo = "";
          String allViewed = props.getProperty("allviewed", "false");

          // if allview=true is in the infofile, mark media as viewed
          if (allViewed.equals("true")) {
            viewInfo = "viewed";
          } else {
            // get viewing percentage from infofile
            int fileViewPercentage = Integer.parseInt(props.getProperty(infoKey, "0"));
            if (fileViewPercentage != 0) {
              viewInfo = "viewed for " + fileViewPercentage + "%";
            }
          }

          // if info was set, draw it on the thumbnail
          if (viewInfo != "") {
            // draw a senitransparent black bar to increase readability
            g.setColor(new Color(0, 0, 0, 190));
            g.fillRect(0, image.getHeight() - 35, image.getWidth(), 35);

            // draw info
            g.setFont(new Font("Arial", Font.PLAIN, 25));
            g.setColor(new Color(240, 240, 240));
            FontMetrics fm = g.getFontMetrics();
            int viewInfoX = (image.getWidth() - fm.stringWidth(viewInfo)) / 2;
            int viewInfoY = image.getHeight() - 7;
            g.drawString(viewInfo, viewInfoX, viewInfoY);

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ImageIO.write(image, "jpeg", out);
            media.setThumb(out.toByteArray());
          }
        } catch (IOException e) {
        }
      }
    } catch (IOException e) {
      log.error("Error while updating thumbnail : " + e.getMessage());
    }
  }

  /**
   * Log Exceptions by first converting it to string, and than logging that string.
   *
   * @param Exeption e
   */
  private void logExeptionError(Exception e) {
    StringWriter writer = new StringWriter();
    e.printStackTrace(new PrintWriter(writer));
    log.error(writer.toString());
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    if (e.getSource() == cbEnableMV) {
      enabledMV = cbEnableMV.isSelected();
      PMSConf.setCustomProperty("enableViewStatus", enabledMV);
    }
  }
}