Package net.pms.dlna

Source Code of net.pms.dlna.DLNAResource

/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2008  A.Brochard
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package net.pms.dlna;

import com.floreysoft.jmte.Engine;

import net.pms.Messages;
import net.pms.PMS;
import net.pms.configuration.FormatConfiguration;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.dlna.virtual.TranscodeVirtualFolder;
import net.pms.dlna.virtual.VirtualFolder;
import net.pms.encoders.*;
import net.pms.external.AdditionalResourceFolderListener;
import net.pms.external.ExternalFactory;
import net.pms.external.ExternalListener;
import net.pms.external.StartStopListener;
import net.pms.formats.Format;
import net.pms.formats.FormatFactory;
import net.pms.io.OutputParams;
import net.pms.io.ProcessWrapper;
import net.pms.io.SizeLimitInputStream;
import net.pms.network.HTTPResource;
import net.pms.util.ImagesUtil;
import net.pms.util.Iso639;
import net.pms.util.MpegUtil;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;

import static net.pms.util.StringUtil.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
* Represents any item that can be browsed via the UPNP ContentDirectory service.
*
* TODO: Change all instance variables to private. For backwards compatibility
* with external plugin code the variables have all been marked as deprecated
* instead of changed to private, but this will surely change in the future.
* When everything has been changed to private, the deprecated note can be
* removed.
*/
public abstract class DLNAResource extends HTTPResource implements Cloneable, Runnable {
  private final Map<String, Integer> requestIdToRefcount = new HashMap<String, Integer>();
  private boolean resolved;

  private static final int STOP_PLAYING_DELAY = 4000;
  private static final Logger logger = LoggerFactory.getLogger(DLNAResource.class);
  private static final SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
  private static final PmsConfiguration configuration = PMS.getConfiguration();
  private static final Engine displayNameTemplateEngine = Engine.createCompilingEngine();

  static {
    displayNameTemplateEngine.setExprStartToken("<");
    displayNameTemplateEngine.setExprEndToken(">");
  }

  protected static final int MAX_ARCHIVE_ENTRY_SIZE = 10000000;
  protected static final int MAX_ARCHIVE_SIZE_SEEK = 800000000;

  /**
   * The name displayed on the renderer. Cached the first time getDisplayName(RendererConfiguration) is called.
   */
  private String displayName;

  /**
   * @deprecated This field will be removed. Use {@link net.pms.configuration.PmsConfiguration#getTranscodeFolderName()} instead.
   */
  @Deprecated
  protected static final String TRANSCODE_FOLDER = Messages.getString("TranscodeVirtualFolder.0"); // localized #--TRANSCODE--#

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected int specificType;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected String id;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected DLNAResource parent;

  /**
   * @deprecated This field will be removed. Use {@link #getFormat()} and
   * {@link #setFormat(Format)} instead.
   */
  @Deprecated
  protected Format ext;

  /**
   * The format of this resource.
   */
  private Format format;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected DLNAMediaInfo media;

  /**
   * @deprecated Use {@link #getMediaAudio()} and {@link
   * #setMediaAudio(DLNAMediaAudio)} to access this field.
   */
  @Deprecated
  protected DLNAMediaAudio media_audio;

  /**
   * @deprecated Use {@link #getMediaSubtitle()} and {@link
   * #setMediaSubtitle(DLNAMediaSubtitle)} to access this field.
   */
  @Deprecated
  protected DLNAMediaSubtitle media_subtitle;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected long lastmodified; // TODO make private and rename lastmodified -> lastModified

  /**
   * Represents the transformation to be used to the file. If null, then
   * @see Player
   */
  private Player player;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected boolean discovered = false;

  private ProcessWrapper externalProcess;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected boolean srtFile;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected int updateId = 1;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  public static int systemUpdateId = 1;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected boolean noName;

  private int nametruncate;
  private DLNAResource first;
  private DLNAResource second;

  /**
   * @deprecated Use standard getter and setter to access this field.
   *
   * The time range for the file containing the start and end time in seconds.
   */
  @Deprecated
  protected Range.Time splitRange = new Range.Time();

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected int splitTrack;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected String fakeParentId;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  // Ditlew - needs this in one of the derived classes
  @Deprecated
  protected RendererConfiguration defaultRenderer;

  private String dlnaspec;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected boolean avisynth;

  /**
   * @deprecated Use standard getter and setter to access this field.
   */
  @Deprecated
  protected boolean skipTranscode = false;

  private boolean allChildrenAreFolders = true;
  private String dlnaOrgOpFlags;

  /**
   * @deprecated Use standard getter and setter to access this field.
   *
   * List of children objects associated with this DLNAResource. This is only valid when the DLNAResource is of the container type.
   */
  @Deprecated
  protected List<DLNAResource> children;

  /**
   * @deprecated Use standard getter and setter to access this field.
   *
   * The numerical ID (1-based index) assigned to the last child of this folder. The next child is assigned this ID + 1.
   */
  // FIXME should be lastChildId
  @Deprecated
  protected int lastChildrenId = 0; // XXX make private and rename lastChildrenId -> lastChildId

  /**
   * @deprecated Use standard getter and setter to access this field.
   *
   * The last time refresh was called.
   */
  @Deprecated
  protected long lastRefreshTime;

  /**
   * Returns parent object, usually a folder type of resource. In the DLDI
   * queries, the UPNP server needs to give out the parent container where
   * the item is. The <i>parent</i> represents such a container.
   *
   * @return Parent object.
   */
  public DLNAResource getParent() {
    return parent;
  }

  /**
   * Set the parent object, usually a folder type of resource. In the DLDI
   * queries, the UPNP server needs to give out the parent container where
   * the item is. The <i>parent</i> represents such a container.

   * @param parent Sets the parent object.
   */
  public void setParent(DLNAResource parent) {
    this.parent = parent;
  }

  /**
   * Returns the id of this resource based on the index in its parent
   * container. Its main purpose is to be unique in the parent container.
   *
   * @return The id string.
   * @since 1.50.0
   */
  protected String getId() {
    return id;
  }

  /**
   * Set the ID of this resource based on the index in its parent container.
   * Its main purpose is to be unique in the parent container. The method is
   * automatically called by addChildInternal, so most of the time it is not
   * necessary to call it explicitly.
   *
   * @param id
   * @since 1.50.0
   * @see #addChildInternal(DLNAResource)
   */
  protected void setId(String id) {
    this.id = id;
  }

  /**
   * String representing this resource ID. This string is used by the UPNP
   * ContentDirectory service. There is no hard spec on the actual numbering
   * except for the root container that always has to be "0". In PMS the
   * format used is <i>number($number)+</i>. A common client that expects a
   * different format than the one used here is the XBox360. PMS translates
   * the XBox360 queries on the fly. For more info, check
   * http://www.mperfect.net/whsUpnp360/ .
   *
   * @return The resource id.
   * @since 1.50.0
   */
  public String getResourceId() {
    if (getId() == null) {
      return null;
    }

    if (getParent() != null) {
      return getParent().getResourceId() + '$' + getId();
    } else {
      return getId();
    }
  }

  /**
   * @see #setId(String)
   * @param id
   */
  protected void setIndexId(int id) {
    setId(Integer.toString(id));
  }

  /**
   *
   * @return the unique id which identifies the DLNAResource relative to its parent.
   */
  public String getInternalId() {
    return getId();
  }

  /**
   *
   * @return true, if this contain can have a transcode folder
   */
  // TODO (breaking change): should be protected
  public boolean isTranscodeFolderAvailable() {
    return true;
  }

  /**
   * Any {@link DLNAResource} needs to represent the container or item with a String.
   *
   * @return String to be showed in the UPNP client.
   */
  public abstract String getName();

  public abstract String getSystemName();

  public abstract long length();

  // Ditlew
  public long length(RendererConfiguration mediaRenderer) {
    return length();
  }

  public abstract InputStream getInputStream() throws IOException;

  public abstract boolean isFolder();

  public String getDlnaContentFeatures() {
    return (dlnaspec != null ? (dlnaspec + ";") : "") + getDlnaOrgOpFlags() + ";DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000";
  }

  public DLNAResource getPrimaryResource() {
    return first;
  }

  public DLNAResource getSecondaryResource() {
    return second;
  }

  public String getFakeParentId() {
    return fakeParentId;
  }

  public void setFakeParentId(String fakeParentId) {
    this.fakeParentId = fakeParentId;
  }

  /**
   * @return the fake parent id if specified, or the real parent id
   */
  public String getParentId() {
    if (getFakeParentId() != null) {
      return getFakeParentId();
    } else {
      if (getParent() != null) {
        return getParent().getResourceId();
      } else {
        return "-1";
      }
    }
  }

  public DLNAResource() {
    setSpecificType(Format.UNKNOWN);
    setChildren(new ArrayList<DLNAResource>());
    setUpdateId(1);
  }

  public DLNAResource(int specificType) {
    this();
    setSpecificType(specificType);
  }

  /**
   * Recursive function that searches through all of the children until it finds
   * a {@link DLNAResource} that matches the name.<p> Only used by
   * {@link net.pms.dlna.RootFolder#addWebFolder(File webConf)
   * addWebFolder(File webConf)} while parsing the web.conf file.
   * @param name String to be compared the name to.
   * @return Returns a {@link DLNAResource} whose name matches the parameter name
   * @see #getName()
   */
  public DLNAResource searchByName(String name) {
    for (DLNAResource child : getChildren()) {
      if (child.getName().equals(name)) {
        return child;
      }
    }

    return null;
  }

  /**
   * @param renderer Renderer for which to check if file is supported.
   * @return true if the given {@link net.pms.configuration.RendererConfiguration
   *    RendererConfiguration} can understand type of media. Also returns true
   *    if this DLNAResource is a container.
   */
  public boolean isCompatible(RendererConfiguration renderer) {
    return getFormat() == null
      || getFormat().isUnknown()
      || (getFormat().isVideo() && renderer.isVideoSupported())
      || (getFormat().isAudio() && renderer.isAudioSupported())
      || (getFormat().isImage() && renderer.isImageSupported());
  }

  /**
   * Adds a new DLNAResource to the child list. Only useful if this object is
   * of the container type.
   * <P>
   * TODO: (botijo) check what happens with the child object. This function
   * can and will transform the child object. If the transcode option is set,
   * the child item is converted to a container with the real item and the
   * transcode option folder. There is also a parser in order to get the right
   * name and type, I suppose. Is this the right place to be doing things like
   * these?
   * <p>
   * FIXME: Ideally the logic below is completely renderer-agnostic. Focus on
   * harvesting generic data and transform it for a specific renderer as late
   * as possible.
   *
   * @param child
   *            DLNAResource to add to a container type.
   */
  public void addChild(DLNAResource child) {
    // child may be null (spotted - via rootFolder.addChild() - in a misbehaving plugin
    if (child == null) {
      logger.error("A plugin has attempted to add a null child to \"{}\"", getName());
      logger.debug("Error info:", new NullPointerException("Invalid DLNA resource"));
      return;
    }

    child.setParent(this);

    if (getParent() != null) {
      setDefaultRenderer(getParent().getDefaultRenderer());
    }

    try {
      if (child.isValid()) {
        logger.trace("Adding new child \"{}\" with class \"{}\"", child.getName(), child.getClass().getName());

        if (allChildrenAreFolders && !child.isFolder()) {
          allChildrenAreFolders = false;
        }

        addChildInternal(child);

        boolean parserV2 = child.getMedia() != null && getDefaultRenderer() != null && getDefaultRenderer().isMediaParserV2();

        if (parserV2) {
          // See which mime type the renderer prefers in case it supports the media
          String mimeType = getDefaultRenderer().getFormatConfiguration().match(child.getMedia());

          if (mimeType != null) {
            // Media is streamable
            if (!FormatConfiguration.MIMETYPE_AUTO.equals(mimeType)) {
              // Override with the preferred mime type of the renderer
              logger.trace("Overriding detected mime type \"{}\" for file \"{}\" with renderer preferred mime type \"{}\"",
                  child.getMedia().getMimeType(), child.getName(), mimeType);
              child.getMedia().setMimeType(mimeType);
            }

            logger.trace("File \"{}\" can be streamed with mime type \"{}\"", child.getName(), child.getMedia().getMimeType());
          } else {
            // Media is transcodable
            logger.trace("File \"{}\" can be transcoded", child.getName());
          }
        }

        if (child.getFormat() != null) {
          String configurationSkipExtensions = configuration.getDisableTranscodeForExtensions();
          String rendererSkipExtensions = null;
         
          if (getDefaultRenderer() != null) {
            rendererSkipExtensions = getDefaultRenderer().getStreamedExtensions();
          }

          // Should transcoding be skipped for this format?
          boolean skip = child.getFormat().skip(configurationSkipExtensions, rendererSkipExtensions);
          setSkipTranscode(skip);
         
          if (skip) {
            logger.trace("File \"{}\" will be forced to skip transcoding by configuration", child.getName());
          }

          if (parserV2 || (child.getFormat().transcodable() && child.getMedia() == null)) {
            if (!parserV2) {
              child.setMedia(new DLNAMediaInfo());
            }
 
            // Try to determine a player to use for transcoding.
            Player player = null;
 
            // First, try to match a player based on the name of the DLNAResource
            // or its parent. If the name ends in "[unique player id]", that player
            // is preferred.
            String name = getName();
 
            for (Player p : PlayerFactory.getAllPlayers()) {
              String end = "[" + p.id() + "]";
 
              if (name.endsWith(end)) {
                nametruncate = name.lastIndexOf(end);
                player = p;
                logger.trace("Selecting player based on name end");
                break;
              } else if (getParent() != null && getParent().getName().endsWith(end)) {
                getParent().nametruncate = getParent().getName().lastIndexOf(end);
                player = p;
                logger.trace("Selecting player based on parent name end");
                break;
              }
            }
 
            // If no preferred player could be determined from the name, try to
            // match a player based on media information and format.
            if (player == null) {
              player = PlayerFactory.getPlayer(child);
            }
 
            if (player != null && !allChildrenAreFolders) {
              String configurationForceExtensions = configuration.getForceTranscodeForExtensions();
              String rendererForceExtensions = null;
             
              if (getDefaultRenderer() != null) {
                rendererForceExtensions = getDefaultRenderer().getTranscodedExtensions();
              }

              // Should transcoding be forced for this format?
              boolean forceTranscode = child.getFormat().skip(configurationForceExtensions, rendererForceExtensions);

              if (forceTranscode) {
                logger.trace("File \"{}\" will be forced to be transcoded by configuration", child.getName());
              }

              boolean hasEmbeddedSubs = false;
 
              if (child.getMedia() != null) {
                for (DLNAMediaSubtitle s : child.getMedia().getSubtitleTracksList()) {
                  hasEmbeddedSubs = (hasEmbeddedSubs || s.isEmbedded());
                }
              }
 
              boolean hasSubsToTranscode = false;
 
              if (!configuration.isDisableSubtitles()) {
                // FIXME: Why transcode if the renderer can handle embedded subs?
                hasSubsToTranscode = (configuration.isAutoloadExternalSubtitles() && child.isSrtFile()) || hasEmbeddedSubs;

                if (hasSubsToTranscode) {
                  logger.trace("File \"{}\" has subs that need transcoding", child.getName());
                }
              }
 
              boolean isIncompatible = false;
 
              if (!child.getFormat().isCompatible(child.getMedia(), getDefaultRenderer())) {
                isIncompatible = true;
                logger.trace("File \"{}\" is not supported by the renderer", child.getName());
              }
 
              // Prefer transcoding over streaming if:
              // 1) the media is unsupported by the renderer, or
              // 2) there are subs to transcode
              boolean preferTranscode = isIncompatible || hasSubsToTranscode;

              // Transcode if:
              // 1) transcoding is forced by configuration, or
              // 2) transcoding is preferred and not prevented by configuration
              if (forceTranscode || (preferTranscode && !isSkipTranscode())) {
                child.setPlayer(player);

                if (parserV2) {
                  logger.trace("Final verdict: \"{}\" will be transcoded with player \"{}\" with mime type \"{}\"", child.getName(), player.toString(), child.getMedia().getMimeType());
                } else {
                  logger.trace("Final verdict: \"{}\" will be transcoded with player \"{}\"", child.getName(), player.toString());
                }
              } else {
                logger.trace("Final verdict: \"{}\" will be streamed", child.getName());
              }
 
              // Should the child be added to the #--TRANSCODE--# folder?
              if ((child.getFormat().isVideo() || child.getFormat().isAudio()) && child.isTranscodeFolderAvailable()) {
                // true: create (and append) the #--TRANSCODE--# folder to this
                // folder if supported/enabled and if it doesn't already exist
                VirtualFolder transcodeFolder = getTranscodeFolder(true);
                if (transcodeFolder != null) {
                  VirtualFolder fileTranscodeFolder = new FileTranscodeVirtualFolder(child.getName(), null);
 
                  DLNAResource newChild = child.clone();
                  newChild.setPlayer(player);
                  newChild.setMedia(child.getMedia());
                  fileTranscodeFolder.addChildInternal(newChild);
                  logger.trace("Adding \"{}\" to transcode folder for player: \"{}\"", child.getName(), player.toString());
 
                  transcodeFolder.addChild(fileTranscodeFolder);
                }
              }
 
              for (ExternalListener listener : ExternalFactory.getExternalListeners()) {
                if (listener instanceof AdditionalResourceFolderListener) {
                  try {
                    ((AdditionalResourceFolderListener) listener).addAdditionalFolder(this, child);
                  } catch (Throwable t) {
                    logger.error("Failed to add additional folder for listener of type: \"{}\"", listener.getClass(), t);
                  }
                }
              }
            } else if (!child.getFormat().isCompatible(child.getMedia(), getDefaultRenderer()) && !child.isFolder()) {
              logger.trace("Ignoring file \"{}\" because it is not compatible with renderer \"{}\"", child.getName(), getDefaultRenderer().getRendererName());
              getChildren().remove(child);
            }
          }

          if (child.getFormat().getSecondaryFormat() != null &&
            child.getMedia() != null &&
            getDefaultRenderer() != null &&
            getDefaultRenderer().supportsFormat(child.getFormat().getSecondaryFormat())
          ) {
            DLNAResource newChild = child.clone();
            newChild.setFormat(newChild.getFormat().getSecondaryFormat());
            logger.trace("Detected secondary format \"{}\" for \"{}\"", newChild.getFormat().toString(), newChild.getName());
            newChild.first = child;
            child.second = newChild;
 
            if (!newChild.getFormat().isCompatible(newChild.getMedia(), getDefaultRenderer())) {
              Player player = PlayerFactory.getPlayer(newChild);
              newChild.setPlayer(player);
              logger.trace("Secondary format \"{}\" will use player \"{}\" for \"{}\"", newChild.getFormat().toString(), child.getPlayer().name(), newChild.getName());
            }
 
            if (child.getMedia() != null && child.getMedia().isSecondaryFormatValid()) {
              addChild(newChild);
              logger.trace("Adding secondary format \"{}\" for \"{}\"", newChild.getFormat().toString(), newChild.getName());
            } else {
              logger.trace("Ignoring secondary format \"{}\" for \"{}\": invalid format", newChild.getFormat().toString(), newChild.getName());
            }
          }
        }
      }
    } catch (Throwable t) {
      logger.error("Error adding child: \"{}\"", child.getName(), t);

      child.setParent(null);
      getChildren().remove(child);
    }
  }

  /**
   * Return the transcode folder for this resource.
   * If PMS is configured to hide transcode folders, null is returned.
   * If no folder exists and the create argument is false, null is returned.
   * If no folder exists and the create argument is true, a new transcode folder is created.
   * This method is called on the parent frolder each time a child is added to that parent
   * (via {@link #addChild(DLNAResource)}.
   *
   * @param create
   * @return the transcode virtual folder
   */
  // XXX package-private: used by MapFile; should be protected?
  TranscodeVirtualFolder getTranscodeFolder(boolean create) {
    if (!isTranscodeFolderAvailable()) {
      return null;
    }

    if (configuration.getHideTranscodeEnabled()) {
      return null;
    }

    // search for transcode folder
    for (DLNAResource child : getChildren()) {
      if (child instanceof TranscodeVirtualFolder) {
        return (TranscodeVirtualFolder) child;
      }
    }

    if (create) {
      TranscodeVirtualFolder transcodeFolder = new TranscodeVirtualFolder(null);
      addChildInternal(transcodeFolder);
      return transcodeFolder;
    }

    return null;
  }

  /**
   * Adds the supplied DNLA resource to the internal list of child nodes,
   * and sets the parent to the current node. Avoids the side-effects
   * associated with the {@link #addChild(DLNAResource)} method.
   *
   * @param child the DLNA resource to add to this node's list of children
   */
  protected synchronized void addChildInternal(DLNAResource child) {
    if (child.getInternalId() != null) {
      logger.info(
        "Node ({}) already has an ID ({}), which is overridden now. The previous parent node was: {}",
        new Object[] {
          child.getClass().getName(),
          child.getResourceId(),
          child.getParent()
        }
      );
    }

    getChildren().add(child);
    child.setParent(this);

    setLastChildId(getLastChildId() + 1);
    child.setIndexId(getLastChildId());
  }

  /**
   * First thing it does it searches for an item matching the given objectID.
   * If children is false, then it returns the found object as the only object in the list.
   * TODO: (botijo) This function does a lot more than this!
   * @param objectId ID to search for.
   * @param returnChildren State if you want all the children in the returned list.
   * @param start
   * @param count
   * @param renderer Renderer for which to do the actions.
   * @return List of DLNAResource items.
   * @throws IOException
   */
  public synchronized List<DLNAResource> getDLNAResources(String objectId, boolean returnChildren, int start, int count, RendererConfiguration renderer) throws IOException {
    ArrayList<DLNAResource> resources = new ArrayList<DLNAResource>();
    DLNAResource dlna = search(objectId, count, renderer);

    if (dlna != null) {
      String systemName = dlna.getSystemName();
      dlna.setDefaultRenderer(renderer);

      if (!returnChildren) {
        resources.add(dlna);
        dlna.refreshChildrenIfNeeded();
      } else {
        dlna.discoverWithRenderer(renderer, count, true);

        if (count == 0) {
          count = dlna.getChildren().size();
        }

        if (count > 0) {
          ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(count);

          int nParallelThreads = 3;
          if (dlna instanceof DVDISOFile) {
            nParallelThreads = 1; // Some DVD drives die wih 3 parallel threads
          }

          ThreadPoolExecutor tpe = new ThreadPoolExecutor(
            Math.min(count, nParallelThreads),
            count,
            20,
            TimeUnit.SECONDS,
            queue
          );

          for (int i = start; i < start + count; i++) {
            if (i < dlna.getChildren().size()) {
              final DLNAResource child = dlna.getChildren().get(i);

              if (child != null) {
                tpe.execute(child);
                resources.add(child);
              } else {
                logger.warn("null child at index {} in {}", i, systemName);
              }
            }
          }

          try {
            tpe.shutdown();
            tpe.awaitTermination(20, TimeUnit.SECONDS);
          } catch (InterruptedException e) {
            logger.error("error while shutting down thread pool executor for " + systemName, e);
          }

          logger.trace("End of analysis for {}", systemName);
        }
      }
    }

    return resources;
  }

  protected void refreshChildrenIfNeeded() {
    if (isDiscovered() && isRefreshNeeded()) {
      refreshChildren();
      notifyRefresh();
    }
  }

  /**
   * Update the last refresh time.
   */
  protected void notifyRefresh() {
    setLastRefreshTime(System.currentTimeMillis());
    setUpdateId(getUpdateId() + 1);
    setSystemUpdateId(getSystemUpdateId() + 1);
  }

  final protected void discoverWithRenderer(RendererConfiguration renderer, int count, boolean forced) {
    // Discover children if it hasn't been done already
    if (!isDiscovered()) {
      discoverChildren();
      boolean ready;

      if (renderer.isMediaParserV2() && renderer.isDLNATreeHack()) {
        ready = analyzeChildren(count);
      } else {
        ready = analyzeChildren(-1);
      }

      if (!renderer.isMediaParserV2() || ready) {
        setDiscovered(true);
      }

      notifyRefresh();
    } else {
      // if forced, then call the old 'refreshChildren' method
      logger.trace("discover {} refresh forced: {}", getResourceId(), forced);
      if (forced) {
        if (refreshChildren()) {
          notifyRefresh();
        }
      } else {
        // if not, then the regular isRefreshNeeded/doRefreshChildren pair.
        if (isRefreshNeeded()) {
          doRefreshChildren();
          notifyRefresh();
        }
      }
    }
  }

  @Override
  public void run() {
    if (first == null) {
      resolve();
      if (second != null) {
        second.resolve();
      }
    }
  }

  /**
   * Recursive function that searches for a given ID.
   *
   * @param searchId ID to search for.
   * @param renderer
   * @param count
   * @return Item found, or null otherwise.
   * @see #getId()
   */
  public DLNAResource search(String searchId, int count, RendererConfiguration renderer) {
    if (getId() != null && searchId != null) {
      String[] indexPath = searchId.split("\\$", 2);
      if (getId().equals(indexPath[0])) {
        if (indexPath.length == 1 || indexPath[1].length() == 0) {
          return this;
        } else {
          discoverWithRenderer(renderer, count, false);

          for (DLNAResource file : getChildren()) {
            DLNAResource found = file.search(indexPath[1], count, renderer);
            if (found != null) {
              return found;
            }
          }
        }
      } else {
        return null;
      }
    }
    return null;
  }

  /**
   * TODO: (botijo) What is the intention of this function? Looks like a prototype to be overloaded.
   */
  public void discoverChildren() { }

  /**
   * TODO: (botijo) What is the intention of this function? Looks like a prototype to be overloaded.
   * @param count
   * @return Returns true
   */
  public boolean analyzeChildren(int count) {
    return true;
  }

  /**
   * Reload the list of children.
   */
  public void doRefreshChildren() { }

  /**
   * @return true, if the container is changed, so refresh is needed.
   * This could be called a lot of times.
   */
  public boolean isRefreshNeeded() {
    return false;
  }

  /**
   * This method gets called only for the browsed folder, and not for the
   * parent folders. (And in the media library scan step too). Override in
   * plugins when you do not want to implement proper change tracking, and
   * you do not care if the hierarchy of nodes getting invalid between.
   *
   * @return True when a refresh is needed, false otherwise.
   */
  public boolean refreshChildren() {
    if (isRefreshNeeded()) {
      doRefreshChildren();
      return true;
    }

    return false;
  }

  /**
   * @deprecated Use {@link #resolveFormat()} instead.
   */
  @Deprecated
  protected void checktype() {
    resolveFormat();
  }

  /**
   * Sets the resource's {@link net.pms.formats.Format} according to its filename
   * if it isn't set already.
   * @since 1.90.0
   */
  protected void resolveFormat() {
    if (getFormat() == null) {
      setFormat(FormatFactory.getAssociatedFormat(getSystemName()));
    }

    if (getFormat() != null && getFormat().isUnknown()) {
      getFormat().setType(getSpecificType());
    }
  }

  /**
   * Hook to lazily initialise immutable resources e.g. ISOs, zip files &amp;c.
   *
   * @since 1.90.0
   * @see #resolve()
   */
  protected void resolveOnce() { }

  /**
   * Resolve events are hooks that allow DLNA resources to perform various forms
   * of initialisation when navigated to or streamed i.e. they function as lazy
   * constructors.
   *
   * This method is called by request handlers for a) requests for a stream
   * or b) content directory browsing i.e. for potentially every request for a file or
   * folder the renderer hasn't cached. Many resource types are immutable (e.g. playlists,
   * zip files, DVD ISOs &amp;c.) and only need to respond to this event once.
   * Most resource types don't "subscribe" to this event at all. This default implementation
   * provides hooks for immutable resources and handles the event for resource types that
   * don't care about it. The rest override this method and handle it accordingly. Currently,
   * the only resource type that overrides it is {@link RealFile}.
   *
   * Note: resolving a resource once (only) doesn't prevent children being added to or
   * removed from it (if supported). There are other mechanisms for that e.g.
   * {@link #doRefreshChildren()} (see {@link Feed} for an example).
   */
  public synchronized void resolve() {
    if (!resolved) {
      resolveOnce();
      // if resolve() isn't overridden, this file/folder is immutable
      // (or doesn't respond to resolve events, which amounts to the
      // same thing), so don't spam it with this event again.
      resolved = true;
    }
  }

  // Ditlew
  /**
   * Returns the display name for the default renderer.
   *
   * @return The display name.
   * @see #getDisplayName(RendererConfiguration)
   */
  public String getDisplayName() {
    return getDisplayName(getDefaultRenderer());
  }

  // helper method for getDisplayName
  private boolean anyStringIsNotBlank(String... strings) {
    for (String string : strings) {
      if (isNotBlank(string)) {
        return true;
      }
    }

    return false;
  }

  /**
   * This hook allows subclasses to add resource-specific variables
   * to the String -&gt; Object map exported to the display name
   * template in {@link #getDisplayName(RendererConfiguration)}.
   *
   * @since 1.90.0
   * @param vars the current map of variables to export to the template
   */
  protected void finalizeDisplayNameVars(Map<String, Object> vars) { }

  /**
   * Returns the string for this resource that will be displayed on the
   * renderer. The name is formatted based on the PMS.conf settings
   * for filename_format_short and filename_format_long, which can
   * be overridden on a per-renderer basis by ShortFilenameFormat
   * and LongFilenameFormat respectively.
   *
   * This allows the same resource to be displayed with different
   * display names on different renderers.
   *
   * See the "Filename templates" section of PMS.conf for full details.
   *
   * @param renderer the renderer the resource will be displayed on.
   * @return the resource's display name.
   */
  public String getDisplayName(RendererConfiguration renderer) {
    if (displayName != null) { // cached
      return displayName;
    }

    // Chapter virtual folder ignores formats and only displays the start time
    if (getSplitRange().isEndLimitAvailable()) {
      displayName = ">> " + DLNAMediaInfo.getDurationString(getSplitRange().getStart());
      return displayName;
    }

    // Is this still relevant? The player name already contains "AviSynth"
    if (isAvisynth()) {
      displayName = (getPlayer() != null ? ("[" + getPlayer().name()) : "") + " + AviSynth]";
      return displayName;
    }

    String template;
    String audioLangFullName = "";
    String audioLangShortName = "";
    String audioFlavor = "";
    String audioCodec = "";
    String dvdTrackDuration = "";
    String engineFullName = "";
    String engineShortName = "";
    String filenameWithExtension = "";
    String filenameWithoutExtension = "";
    String subLangFullName = "";
    String subLangShortName = "";
    String subType = "";
    String subFlavor = "";
    String externalSubs = "";

    // the display names of #--TRANSCODE--# folder files may need to
    // pack in a lot of information (e.g. engine, audio track, subtitle
    // track) so we use a compact format to try to prevent them being
    // truncated or scrolling off the screen
    boolean useShortFormat = isNoName();

    // Determine the format
    if (renderer != null) {
      if (useShortFormat) {
        template = renderer.getShortFilenameFormat();
      } else {
        template = renderer.getLongFilenameFormat();
      }
    } else {
      if (useShortFormat) {
        template = configuration.getShortFilenameFormat();
      } else {
        template = configuration.getLongFilenameFormat();
      }
    }

    // Handle file name
    if (!isNoName()) {
      filenameWithExtension = getName();
      filenameWithoutExtension = FilenameUtils.getBaseName(filenameWithExtension);

      // Check if file extensions are configured to be hidden
      if (this instanceof RealFile && configuration.isHideExtensions() && !isFolder()) {
        filenameWithExtension = filenameWithoutExtension;
      }
    }

    // Handle engine name
    if (isNoName() || !configuration.isHideEngineNames()) {
      if (getPlayer() != null) {
        engineFullName = getPlayer().name();
        engineShortName = abbreviate(engineFullName);
      } else if (isNoName()) {
        engineFullName = Messages.getString("DLNAResource.1");
        engineShortName = Messages.getString("DLNAResource.2");
      }
    }

    // Handle DVD track duration
    if (
      renderer != null
      && renderer.isShowDVDTitleDuration()
      && getMedia() != null
      && getMedia().getDvdtrack() > 0
    ) {
      dvdTrackDuration = getMedia().getDurationString();
    }

    // Handle external subtitles
    if (isSrtFile()
      && (getMediaAudio() == null
      && getMediaSubtitle() == null)
      && (getPlayer() == null || getPlayer().isExternalSubtitlesSupported())
    ) {
      externalSubs = Messages.getString("DLNAResource.0");
    }

    // Handle audio
    if (getMediaAudio() != null) {
      audioCodec = getMediaAudio().getAudioCodec();
      audioLangFullName = getMediaAudio().getLangFullName();
      audioLangShortName = getMediaAudio().getLang();

      if ((getMediaAudio().getFlavor() != null && renderer != null && renderer.isShowAudioMetadata())) {
        audioFlavor = getMediaAudio().getFlavor();
      }
    }

    // Handle subtitle
    if (getMediaSubtitle() != null && getMediaSubtitle().getId() != -1) {
      subType = getMediaSubtitle().getType().getDescription();
      subLangFullName = getMediaSubtitle().getLangFullName();
      subLangShortName = getMediaSubtitle().getLang();

      if (getMediaSubtitle().getFlavor() != null && renderer != null && renderer.isShowSubMetadata()) {
        subFlavor = getMediaSubtitle().getFlavor();
      }
    }

    // define any extra variables to be exported
    boolean isFolder = isFolder();
    boolean extra = anyStringIsNotBlank(dvdTrackDuration, engineShortName, engineFullName, externalSubs, subType);

    Map<String, Object> vars = new HashMap<String, Object>();

    vars.put("lt", "<");
    vars.put("gt", ">");

    vars.put("aLabel", Messages.getString("DLNAResource.3"));
    vars.put("sLabel", Messages.getString("DLNAResource.4"));

    vars.put("aCodec", audioCodec);
    vars.put("aFlavor", audioFlavor);
    vars.put("aFull", audioLangFullName);
    vars.put("aShort", audioLangShortName);
    vars.put("dvdLen", dvdTrackDuration);
    vars.put("eFull", engineFullName);
    vars.put("eShort", engineShortName);
    vars.put("fFull", filenameWithExtension);
    vars.put("fShort", filenameWithoutExtension);
    vars.put("sExt", externalSubs);
    vars.put("sFlavor", subFlavor);
    vars.put("sFull", subLangFullName);
    vars.put("sShort", subLangShortName);
    vars.put("sType", subType);

    vars.put("extra", extra);
    vars.put("isFile", !isFolder);
    vars.put("isFolder", isFolder);

    // allow subclasses to add resource-specific vars
    // currently only used by DVDISOFile
    finalizeDisplayNameVars(vars);

    displayName = displayNameTemplateEngine.transform(template, vars);
    displayName = displayName.replaceAll("\\s+", " ");
    displayName = displayName.trim();

    return displayName;
  }

  /**
   * Prototype for returning URLs.
   *
   * @return An empty URL
   */
  protected String getFileURL() {
    return getURL("");
  }

  /**
   * @return Returns a URL pointing to an image representing the item. If
   * none is available, "thumbnail0000.png" is used.
   */
  protected String getThumbnailURL() {
    StringBuilder sb = new StringBuilder();
    sb.append(PMS.get().getServer().getURL());
    sb.append("/images/");
    String id = null;

    if (getMediaAudio() != null) {
      id = getMediaAudio().getLang();
    }

    if (getMediaSubtitle() != null && getMediaSubtitle().getId() != -1) {
      id = getMediaSubtitle().getLang();
    }

    if ((getMediaSubtitle() != null || getMediaAudio() != null) && StringUtils.isBlank(id)) {
      id = DLNAMediaLang.UND;
    }

    if (id != null) {
      String code = Iso639.getISO639_2Code(id.toLowerCase());
      sb.append("codes/").append(code).append(".png");
      return sb.toString();
    }

    if (isAvisynth()) {
      sb.append("logo-avisynth.png");
      return sb.toString();
    }

    return getURL("thumbnail0000");
  }

  /**
   * @param prefix
   * @return Returns a URL for a given media item. Not used for container types.
   */
  protected String getURL(String prefix) {
    StringBuilder sb = new StringBuilder();
    sb.append(PMS.get().getServer().getURL());
    sb.append("/get/");
    sb.append(getResourceId()); //id
    sb.append("/");
    sb.append(prefix);
    sb.append(encode(getName()));
    return sb.toString();
  }

  /**
   * Transforms a String to UTF-8.
   *
   * @param s
   * @return Transformed string s in UTF-8 encoding.
   */
  private static String encode(String s) {
    try {
      return URLEncoder.encode(s, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      logger.debug("Caught exception", e);
    }
    return "";
  }

  /**
   * @return Number of children objects. This might be used in the DLDI
   * response, as some renderers might not have enough memory to hold the
   * list for all children.
   */
  public int childrenNumber() {
    if (getChildren() == null) {
      return 0;
    }
    return getChildren().size();
  }

  /**
   * (non-Javadoc)
   * @see java.lang.Object#clone()
   */
  @Override
  protected DLNAResource clone() {
    DLNAResource o = null;
    try {
      o = (DLNAResource) super.clone();
      o.setId(null);
      // clear the cached display name
      o.displayName = null;
      // make sure clones (typically #--TRANSCODE--# folder files)
      // have the option to respond to resolve events
      o.resolved = false;
    } catch (CloneNotSupportedException e) {
      logger.error(null, e);
    }

    return o;
  }

  // this shouldn't be public
  @Deprecated
  public String getFlags() {
    return getDlnaOrgOpFlags();
  }

  // permit the renderer to seek by time, bytes or both
  private String getDlnaOrgOpFlags() {
    return "DLNA.ORG_OP=" + dlnaOrgOpFlags;
  }

  /**
   * @deprecated Use {@link #getDidlString(RendererConfiguration)} instead.
   *
   * @param mediaRenderer
   * @return
   */
  @Deprecated
  public final String toString(RendererConfiguration mediaRenderer) {
    return getDidlString(mediaRenderer);
  }

  /**
   * Returns an XML (DIDL) representation of the DLNA node. It gives a
   * complete representation of the item, with as many tags as available.
   * Recommendations as per UPNP specification are followed where possible.
   *
   * @param mediaRenderer
   *            Media Renderer for which to represent this information. Useful
   *            for some hacks.
   * @return String representing the item. An example would start like this:
   *         {@code <container id="0$1" childCount="1" parentID="0" restricted="true">}
   */
  public final String getDidlString(RendererConfiguration mediaRenderer) {
    StringBuilder sb = new StringBuilder();

    if (isFolder()) {
      openTag(sb, "container");
    } else {
      openTag(sb, "item");
    }

    addAttribute(sb, "id", getResourceId());

    if (isFolder()) {
      if (!isDiscovered() && childrenNumber() == 0) {
        //  When a folder has not been scanned for resources, it will automatically have zero children.
        //  Some renderers like XBMC will assume a folder is empty when encountering childCount="0" and
        //  will not display the folder. By returning childCount="1" these renderers will still display
        //  the folder. When it is opened, its children will be discovered and childrenNumber() will be
        //  set to the right value.
        addAttribute(sb, "childCount", 1);
      } else {
        addAttribute(sb, "childCount", childrenNumber());
      }
    }
    addAttribute(sb, "parentID", getParentId());
    addAttribute(sb, "restricted", "true");
    endTag(sb);

    final DLNAMediaAudio firstAudioTrack = getMedia() != null ? getMedia().getFirstAudioTrack() : null;
    if (firstAudioTrack != null && isNotBlank(firstAudioTrack.getSongname())) {
      addXMLTagAndAttribute(
        sb,
        "dc:title",
        encodeXML(firstAudioTrack.getSongname() + (getPlayer() != null && !configuration.isHideEngineNames() ? (" [" + getPlayer().name() + "]") : ""))
      );
    } else { // Ditlew - org
      // Ditlew
      addXMLTagAndAttribute(
        sb,
        "dc:title",
        encodeXML(((isFolder() || getPlayer() == null) ? getDisplayName(mediaRenderer) : mediaRenderer.getUseSameExtension(getDisplayName(mediaRenderer))))
      );
    }

    if (firstAudioTrack != null) {
      if (isNotBlank(firstAudioTrack.getAlbum())) {
        addXMLTagAndAttribute(sb, "upnp:album", encodeXML(firstAudioTrack.getAlbum()));
      }

      if (isNotBlank(firstAudioTrack.getArtist())) {
        addXMLTagAndAttribute(sb, "upnp:artist", encodeXML(firstAudioTrack.getArtist()));
        addXMLTagAndAttribute(sb, "dc:creator", encodeXML(firstAudioTrack.getArtist()));
      }

      if (isNotBlank(firstAudioTrack.getGenre())) {
        addXMLTagAndAttribute(sb, "upnp:genre", encodeXML(firstAudioTrack.getGenre()));
      }

      if (firstAudioTrack.getTrack() > 0) {
        addXMLTagAndAttribute(sb, "upnp:originalTrackNumber", "" + firstAudioTrack.getTrack());
      }
    }

    if (!isFolder()) {
      int indexCount = 1;

      if (mediaRenderer.isDLNALocalizationRequired()) {
        indexCount = getDLNALocalesCount();
      }

      for (int c = 0; c < indexCount; c++) {
        openTag(sb, "res");

        /*
          DLNA.ORG_OP flags

          Two booleans (binary digits) which determine what transport operations the renderer is allowed to
          perform (in the form of HTTP request headers): the first digit allows the renderer to send
          TimeSeekRange.DLNA.ORG (seek by time) headers; the second allows it to send RANGE (seek by byte)
          headers.

            00 - no seeking (or even pausing) allowed
            01 - seek by byte
            10 - seek by time
            11 - seek by both

          See here for an example of how these options can be mapped to keys on the renderer's controller:
          http://www.ps3mediaserver.org/forum/viewtopic.php?f=2&t=2908&p=12550#p12550

          Note that seek-by-byte is the preferred option for streamed files [1] and seek-by-time is the
          preferred option for transcoded files.

          [1] see http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=15841&p=76201#p76201

          seek-by-time requires a) support by the renderer (via the SeekByTime renderer conf option)
          and b) support by the transcode engine.

          The seek-by-byte fallback doesn't work well with transcoded files [2], but it's better than
          disabling seeking (and pausing) altogether.

          [2] http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=3507&p=16567#p16567 (bottom post)
        */

        dlnaOrgOpFlags = "01"; // seek by byte (exclusive)

        if (mediaRenderer.isSeekByTime() && getPlayer() != null && getPlayer().isTimeSeekable()) {
          /*
            Some renderers - e.g. the PS3 and Panasonic TVs - behave erratically when
            transcoding if we keep the default seek-by-byte permission on when permitting
            seek-by-time: http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=15841

            It's not clear if this is a bug in the DLNA libraries of these renderers or a bug
            in PMS, but setting an option in the renderer conf that disables seek-by-byte when
            we permit seek-by-time - e.g.:

              SeekByTime = exclusive

            - works around it.
          */

          /*
            TODO (e.g. in a beta release): set seek-by-time (exclusive) here for *all* renderers:
            seek-by-byte isn't needed here (both the renderer and the engine support seek-by-time)
            and may be buggy on other renderers than the ones we currently handle.

            In the unlikely event that a renderer *requires* seek-by-both here, it can
            opt in with (e.g.):

              SeekByTime = both
          */
          if (mediaRenderer.isSeekByTimeExclusive()) {
            dlnaOrgOpFlags = "10"; // seek by time (exclusive)
          } else {
            dlnaOrgOpFlags = "11"; // seek by both
          }
        }

        addAttribute(sb, "xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/");

        // FIXME: There is a flaw here. In addChild(DLNAResource) the mime type
        // is determined for the default renderer. This renderer may rewrite the
        // mime type based on its configuration. Looking up that mime type is
        // not guaranteed to return a match for another renderer.
        String mime = mediaRenderer.getMimeType(mimeType());

        if (mime == null) {
          // FIXME: Setting the default to "video/mpeg" leaves a lot of audio files in the cold.
          mime = "video/mpeg";
        }

        // XXX remove
        logger.trace("DIDL mime type = " + mime + ", mimeType() = " + mimeType() + " for " + getName());

        dlnaspec = null;

        if (mediaRenderer.isDLNAOrgPNUsed()) {
          if (mediaRenderer.isPS3()) {
            if (mime.equals("video/x-divx")) {
              dlnaspec = "DLNA.ORG_PN=AVI";
            } else if (mime.equals("video/x-ms-wmv") && getMedia() != null && getMedia().getHeight() > 700) {
              dlnaspec = "DLNA.ORG_PN=WMVHIGH_PRO";
            }
          } else {
            if (mime.equals("video/mpeg")) {
              dlnaspec = "DLNA.ORG_PN=" + getMPEG_PS_PALLocalizedValue(c);

              if (getPlayer() != null) {
                // Do we have some mpegts to offer?
                boolean mpegTsMux = TsMuxeRVideo.ID.equals(getPlayer().id()) || VideoLanVideoStreaming.ID.equals(getPlayer().id());
                boolean isMuxableResult = getMedia().isMuxable(mediaRenderer);
                if (!mpegTsMux) {
                  mpegTsMux = MEncoderVideo.ID.equals(getPlayer().id()) && mediaRenderer.isTranscodeToMPEGTSAC3();
                }
                if (mpegTsMux) {
                  dlnaspec = "DLNA.ORG_PN=" + getMPEG_TS_SD_EU_ISOLocalizedValue(c);
                  if (getMedia().isH264()
                      && !VideoLanVideoStreaming.ID.equals(getPlayer().id())
                      && isMuxableResult) {
                    dlnaspec = "DLNA.ORG_PN=AVC_TS_HD_24_AC3_ISO";
                  }
                }
              } else if (getMedia() != null) {
                if (getMedia().isMpegTS()) {
                    dlnaspec = "DLNA.ORG_PN=" + getMPEG_TS_SD_EULocalizedValue(c);
                  if (getMedia().isH264()) {
                    dlnaspec = "DLNA.ORG_PN=AVC_TS_HD_50_AC3";
                  }
                }
              }
            } else if (mime.equals("video/vnd.dlna.mpeg-tts")) {
              // patters - on Sony BDP m2ts clips aren't listed without this
              dlnaspec = "DLNA.ORG_PN=" + getMPEG_TS_SD_EULocalizedValue(c);
            } else if (mime.equals("image/jpeg")) {
              dlnaspec = "DLNA.ORG_PN=JPEG_LRG";
            } else if (mime.equals("audio/mpeg")) {
              dlnaspec = "DLNA.ORG_PN=MP3";
            } else if (mime.substring(0, 9).equals("audio/L16") || mime.equals("audio/wav")) {
              dlnaspec = "DLNA.ORG_PN=LPCM";
            }
          }

          if (dlnaspec != null) {
            dlnaspec = "DLNA.ORG_PN=" + mediaRenderer.getDLNAPN(dlnaspec.substring(12));
          }
        }

        String tempString = "http-get:*:" + mime + ":" + (dlnaspec != null ? (dlnaspec + ";") : "") + getDlnaOrgOpFlags();
        addAttribute(sb, "protocolInfo", tempString);

        if (getFormat() != null && getFormat().isVideo() && getMedia() != null && getMedia().isMediaparsed()) {
          if (getPlayer() == null && getMedia() != null) {
            addAttribute(sb, "size", getMedia().getSize());
          } else {
            long transcoded_size = mediaRenderer.getTranscodedSize();
            if (transcoded_size != 0) {
              addAttribute(sb, "size", transcoded_size);
            }
          }
          if (getMedia().getDuration() != null) {
            if (getSplitRange().isEndLimitAvailable()) {
              addAttribute(sb, "duration", DLNAMediaInfo.getDurationString(getSplitRange().getDuration()));
            } else {
              addAttribute(sb, "duration", getMedia().getDurationString());
            }
          }
          if (getMedia().getResolution() != null) {
            addAttribute(sb, "resolution", getMedia().getResolution());
          }
          addAttribute(sb, "bitrate", getMedia().getRealVideoBitrate());
          if (firstAudioTrack != null) {
            if (firstAudioTrack.getAudioProperties().getNumberOfChannels() > 0) {
              addAttribute(sb, "nrAudioChannels", firstAudioTrack.getAudioProperties().getNumberOfChannels());
            }
            if (firstAudioTrack.getSampleFrequency() != null) {
              addAttribute(sb, "sampleFrequency", firstAudioTrack.getSampleFrequency());
            }
          }
        } else if (getFormat() != null && getFormat().isImage()) {
          if (getMedia() != null && getMedia().isMediaparsed()) {
            addAttribute(sb, "size", getMedia().getSize());
            if (getMedia().getResolution() != null) {
              addAttribute(sb, "resolution", getMedia().getResolution());
            }
          } else {
            addAttribute(sb, "size", length());
          }
        } else if (getFormat() != null && getFormat().isAudio()) {
          if (getMedia() != null && getMedia().isMediaparsed()) {
            addAttribute(sb, "bitrate", getMedia().getBitrate());
            if (getMedia().getDuration() != null) {
              addAttribute(sb, "duration", DLNAMediaInfo.getDurationString(getMedia().getDuration()));
            }
            if (firstAudioTrack != null && firstAudioTrack.getSampleFrequency() != null) {
              addAttribute(sb, "sampleFrequency", firstAudioTrack.getSampleFrequency());
            }
            if (firstAudioTrack != null) {
              addAttribute(sb, "nrAudioChannels", firstAudioTrack.getAudioProperties().getNumberOfChannels());
            }

            if (getPlayer() == null) {
              addAttribute(sb, "size", getMedia().getSize());
            } else {
              // Calculate WAV size
              if (firstAudioTrack != null) {
                int defaultFrequency = mediaRenderer.isTranscodeAudioTo441() ? 44100 : 48000;
                if (!configuration.isAudioResample()) {
                  try {
                    // FIXME: Which exception could be thrown here?
                    defaultFrequency = firstAudioTrack.getSampleRate();
                  } catch (Exception e) {
                    logger.debug("Caught exception", e);
                  }
                }
                int na = firstAudioTrack.getAudioProperties().getNumberOfChannels();
                if (na > 2) { // No 5.1 dump in MPlayer
                  na = 2;
                }
                int finalSize = (int) (getMedia().getDurationInSeconds() * defaultFrequency * 2 * na);
                logger.trace("Calculated size for {}: {}", getSystemName(), finalSize);
                addAttribute(sb, "size", finalSize);
              }
            }
          } else {
            addAttribute(sb, "size", length());
          }
        } else {
          addAttribute(sb, "size", DLNAMediaInfo.TRANS_SIZE);
          addAttribute(sb, "duration", "09:59:59");
          addAttribute(sb, "bitrate", "1000000");
        }
        endTag(sb);
        sb.append(getFileURL());
        closeTag(sb, "res");
      }
    }

    appendThumbnail(mediaRenderer, sb);

    if (getLastModified() > 0) {
      addXMLTagAndAttribute(sb, "dc:date", sdfDate.format(new Date(getLastModified())));
    }

    String uclass;
    if (first != null && getMedia() != null && !getMedia().isSecondaryFormatValid()) {
      uclass = "dummy";
    } else {
      if (isFolder()) {
        uclass = "object.container.storageFolder";
        boolean xbox = mediaRenderer.isXBOX();
        if (xbox && getFakeParentId() != null && getFakeParentId().equals("7")) {
          uclass = "object.container.album.musicAlbum";
        } else if (xbox && getFakeParentId() != null && getFakeParentId().equals("6")) {
          uclass = "object.container.person.musicArtist";
        } else if (xbox && getFakeParentId() != null && getFakeParentId().equals("5")) {
          uclass = "object.container.genre.musicGenre";
        } else if (xbox && getFakeParentId() != null && getFakeParentId().equals("F")) {
          uclass = "object.container.playlistContainer";
        }
      } else if (getFormat() != null && getFormat().isVideo()) {
        uclass = "object.item.videoItem";
      } else if (getFormat() != null && getFormat().isImage()) {
        uclass = "object.item.imageItem.photo";
      } else if (getFormat() != null && getFormat().isAudio()) {
        uclass = "object.item.audioItem.musicTrack";
      } else {
        uclass = "object.item.videoItem";
      }
    }
    addXMLTagAndAttribute(sb, "upnp:class", uclass);

    if (isFolder()) {
      closeTag(sb, "container");
    } else {
      closeTag(sb, "item");
    }
    return sb.toString();
  }

  /**
   * Generate and append the response for the thumbnail based on the
   * configuration of the renderer.
   *
   * @param mediaRenderer The renderer configuration.
   * @param sb            The StringBuilder to append the response to.
   */
  private void appendThumbnail(RendererConfiguration mediaRenderer, StringBuilder sb) {
    final String thumbURL = getThumbnailURL();
    final boolean addThumbnailAsResElement = isFolder() || mediaRenderer.getThumbNailAsResource() || mediaRenderer.isForceJPGThumbnails();

    if (isNotBlank(thumbURL)) {
      if (addThumbnailAsResElement) {
        // Samsung 2012 (ES and EH) models do not recognize the "albumArtURI" element. Instead,
        // the "res" element should be used.
        // Also use "res" when faking JPEG thumbs.
        openTag(sb, "res");

        if (getThumbnailContentType().equals(PNG_TYPEMIME) && !mediaRenderer.isForceJPGThumbnails()) {
          addAttribute(sb, "protocolInfo", "http-get:*:image/png:DLNA.ORG_PN=PNG_TN");
        } else {
          addAttribute(sb, "protocolInfo", "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN");
        }

        endTag(sb);
        sb.append(thumbURL);
        closeTag(sb, "res");
      } else {
        // Renderers that can handle the "albumArtURI" element.
        openTag(sb, "upnp:albumArtURI");
        addAttribute(sb, "xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/");

        if (getThumbnailContentType().equals(PNG_TYPEMIME)) {
          addAttribute(sb, "dlna:profileID", "PNG_TN");
        } else {
          addAttribute(sb, "dlna:profileID", "JPEG_TN");
        }

        endTag(sb);
        sb.append(thumbURL);
        closeTag(sb, "upnp:albumArtURI");
      }
    }
  }

  private String getRequestId(String rendererId) {
    return String.format("%s|%x|%s", rendererId, hashCode(), getSystemName());
  }

  /**
   * Plugin implementation. When this item is going to play, it will notify all
   * the StartStopListener objects available.
   * @see StartStopListener
   */
  public void startPlaying(final String rendererId) {
    final String requestId = getRequestId(rendererId);
    synchronized (requestIdToRefcount) {
      Integer temp = requestIdToRefcount.get(requestId);
      if (temp == null) {
        temp = 0;
      }

      final Integer refCount = temp;
      requestIdToRefcount.put(requestId, refCount + 1);

      if (refCount == 0) {
        final DLNAResource self = this;
        Runnable r = new Runnable() {
          @Override
          public void run() {
            InetAddress rendererIpAddress;
            try {
              rendererIpAddress = InetAddress.getByName(rendererId);
              RendererConfiguration renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(rendererIpAddress);
              String rendererName = "unknown renderer";
              try {
                rendererName = renderer.getRendererName();
              } catch (NullPointerException e) { }
              logger.info("Started sending {} to {} on {}", getSystemName(), rendererName, rendererId);
            } catch (UnknownHostException ex) {
              logger.debug("" + ex);
            }

            for (final ExternalListener listener : ExternalFactory.getExternalListeners()) {
              if (listener instanceof StartStopListener) {
                // run these asynchronously for slow handlers (e.g. logging, scrobbling)
                // TODO use an event bus to fire these
                Runnable fireStartStopEvent = new Runnable() {
                  @Override
                  public void run() {
                    try {
                      ((StartStopListener) listener).nowPlaying(getMedia(), self);
                    } catch (Throwable t) {
                      logger.error("Notification of startPlaying event failed for StartStopListener {}", listener.getClass(), t);
                    }
                  }
                };
                new Thread(fireStartStopEvent, "StartPlaying Event for " + listener.name()).start();
              }
            }
          }
        };

        new Thread(r, "StartPlaying Event").start();
      }
    }
  }

  /**
   * Plugin implementation. When this item is going to stop playing, it will notify all the StartStopListener
   * objects available.
   * @see StartStopListener
   */
  public void stopPlaying(final String rendererId) {
    final DLNAResource self = this;
    final String requestId = getRequestId(rendererId);
    Runnable defer = new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(STOP_PLAYING_DELAY);
        } catch (InterruptedException e) {
          logger.error("stopPlaying sleep interrupted", e);
        }

        synchronized (requestIdToRefcount) {
          final Integer refCount = requestIdToRefcount.get(requestId);
          assert refCount != null;
          assert refCount > 0;
          requestIdToRefcount.put(requestId, refCount - 1);

          Runnable r = new Runnable() {
            @Override
            public void run() {
              if (refCount == 1) {
                InetAddress rendererIpAddress;
                try {
                  rendererIpAddress = InetAddress.getByName(rendererId);
                  RendererConfiguration renderer = RendererConfiguration.getRendererConfigurationBySocketAddress(rendererIpAddress);
                  String rendererName = "unknown renderer";
                  try {
                    rendererName = renderer.getRendererName();
                  } catch (NullPointerException e) { }
                  logger.info("Stopped sending {} to {} on {}", getSystemName(), rendererName, rendererId);
                } catch (UnknownHostException ex) {
                  logger.debug("" + ex);
                }

                PMS.get().getFrame().setStatusLine("");

                for (final ExternalListener listener : ExternalFactory.getExternalListeners()) {
                  if (listener instanceof StartStopListener) {
                    // run these asynchronously for slow handlers (e.g. logging, scrobbling)
                    // TODO use an event bus to fire these
                    Runnable fireStartStopEvent = new Runnable() {
                      @Override
                      public void run() {
                        try {
                          ((StartStopListener) listener).donePlaying(getMedia(), self);
                        } catch (Throwable t) {
                          logger.error("Notification of donePlaying event failed for StartStopListener {}", listener.getClass(), t);
                        }
                      }
                    };
                    new Thread(fireStartStopEvent, "StopPlaying Event for " + listener.name()).start();
                  }
                }
              }
            }
          };

          new Thread(r, "StopPlaying Event").start();
        }
      }
    };

    new Thread(defer, "StopPlaying Event Deferrer").start();
  }

  /**
   * Returns an InputStream of this DLNAResource that starts at a given time, if possible. Very useful if video chapters are being used.
   * @param range
   * @param mediarenderer
   * @return The inputstream
   * @throws IOException
   */
  public InputStream getInputStream(Range range, RendererConfiguration mediarenderer) throws IOException {
    logger.trace("Asked stream chunk : " + range + " of " + getName() + " and player " + getPlayer());

    // shagrath: small fix, regression on chapters
    boolean timeseek_auto = false;
    // Ditlew - WDTV Live
    // Ditlew - We convert byteoffset to timeoffset here. This needs the stream to be CBR!
    int cbr_video_bitrate = mediarenderer.getCBRVideoBitrate();
    long low = range.isByteRange() && range.isStartOffsetAvailable() ? range.asByteRange().getStart() : 0;
    long high = range.isByteRange() && range.isEndLimitAvailable() ? range.asByteRange().getEnd() : -1;
    Range.Time timeRange = range.createTimeRange();

    if (getPlayer() != null && low > 0 && cbr_video_bitrate > 0) {
      int used_bit_rated = (int) ((cbr_video_bitrate + 256) * 1024 / 8 * 1.04); // 1.04 = container overhead
      if (low > used_bit_rated) {
        timeRange.setStart((double) (low / (used_bit_rated)));
        low = 0;

        // WDTV Live - if set to TS it asks multiple times and ends by
        // asking for an invalid offset which kills MEncoder
        if (timeRange.getStartOrZero() > getMedia().getDurationInSeconds()) {
          return null;
        }

        // Should we rewind a little (in case our overhead isn't accurate enough)
        int rewind_secs = mediarenderer.getByteToTimeseekRewindSeconds();
        timeRange.rewindStart(rewind_secs);

        // shagrath:
        timeseek_auto = true;
      }
    }

    // determine source of the stream
    if (getPlayer() == null) {
      // no transcoding
      if (this instanceof IPushOutput) {
        PipedOutputStream out = new PipedOutputStream();
        InputStream fis = new PipedInputStream(out);
        ((IPushOutput) this).push(out);

        if (low > 0) {
          fis.skip(low);
        }
        // http://www.ps3mediaserver.org/forum/viewtopic.php?f=11&t=12035
        fis = wrap(fis, high, low);

        return fis;
      }

      InputStream fis;
      if (getFormat() != null && getFormat().isImage() && getMedia() != null && getMedia().getOrientation() > 1 && mediarenderer.isAutoRotateBasedOnExif()) {
        // seems it's a jpeg file with an orientation setting to take care of
        fis = ImagesUtil.getAutoRotateInputStreamImage(getInputStream(), getMedia().getOrientation());
        if (fis == null) { // error, let's return the original one
          fis = getInputStream();
        }
      } else {
        fis = getInputStream();
      }

      if (fis != null) {
        if (low > 0) {
          fis.skip(low);
        }

        // http://www.ps3mediaserver.org/forum/viewtopic.php?f=11&t=12035
        fis = wrap(fis, high, low);

        if (timeRange.getStartOrZero() > 0 && this instanceof RealFile) {
          fis.skip(MpegUtil.getPositionForTimeInMpeg(((RealFile) this).getFile(), (int) timeRange.getStartOrZero() ));
        }
      }
      return fis;
    } else {
      // pipe transcoding result
      OutputParams params = new OutputParams(configuration);
      params.aid = getMediaAudio();
      params.sid = getMediaSubtitle();
      params.mediaRenderer = mediarenderer;
      timeRange.limit(getSplitRange());
      params.timeseek = timeRange.getStartOrZero();
      params.timeend = timeRange.getEndOrZero();
      params.shift_scr = timeseek_auto;

      if (this instanceof IPushOutput) {
        params.stdin = (IPushOutput) this;
      }

      // (re)start transcoding process if necessary
      if (externalProcess == null || externalProcess.isDestroyed()) {
        // first playback attempt => start new transcoding process
        logger.info("Starting transcode/remux of " + getName());
        logger.debug("Launching transcode with media info: " + getMedia().toString());

        externalProcess = getPlayer().launchTranscode(this, getMedia(), params);

        if (params.waitbeforestart > 0) {
          logger.trace("Sleeping for {} milliseconds", params.waitbeforestart);
          try {
            Thread.sleep(params.waitbeforestart);
          } catch (InterruptedException e) {
            logger.error(null, e);
          }
          logger.trace("Finished sleeping for " + params.waitbeforestart + " milliseconds");
        }
      } else if (params.timeseek > 0 && getMedia() != null && getMedia().isMediaparsed()
          && getMedia().getDurationInSeconds() > 0) {
        // time seek request => stop running transcode process and start new one
        logger.debug("Requesting time seek: " + params.timeseek + " seconds");
        params.minBufferSize = 1;
        Runnable r = new Runnable() {
          @Override
          public void run() {
            externalProcess.stopProcess();
          }
        };
        new Thread(r, "External Process Stopper").start();
        ProcessWrapper newExternalProcess = getPlayer().launchTranscode(this, getMedia(), params);
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          logger.error(null, e);
        }
        if (newExternalProcess == null) {
          logger.trace("External process instance is null... sounds not good");
        }
        externalProcess = newExternalProcess;
      }
      if (externalProcess == null) {
        return null;
      }
      InputStream is = null;
      int timer = 0;
      while (is == null && timer < 10) {
        is = externalProcess.getInputStream(low);
        timer++;
        if (is == null) {
          logger.warn("External input stream instance is null... sounds not good, waiting 500ms");
          try {
            Thread.sleep(500);
          } catch (InterruptedException e) {
          }
        }
      }

      // fail fast: don't leave a process running indefinitely if it's
      // not producing output after params.waitbeforestart milliseconds + 5 seconds
      // this cleans up lingering MEncoder web video transcode processes that hang
      // instead of exiting
      if (is == null && externalProcess != null && !externalProcess.isDestroyed()) {
        Runnable r = new Runnable() {
          @Override
          public void run() {
            logger.error("External input stream instance is null... stopping process");
            externalProcess.stopProcess();
          }
        };
        new Thread(r, "Hanging External Process Stopper").start();
      }
      return is;
    }
  }

  /**
   * Wrap an {@link InputStream} in a {@link SizeLimitInputStream} that sets a
   * limit to the maximum number of bytes to be read from the original input
   * stream. The number of bytes is determined by the high and low value
   * (bytes = high - low). If the high value is less than the low value, the
   * input stream is not wrapped and returned as is.
   *
   * @param input
   *            The input stream to wrap.
   * @param high
   *            The high value.
   * @param low
   *            The low value.
   * @return The resulting input stream.
   */
  private InputStream wrap(InputStream input, long high, long low) {
    if (input != null && high > low) {
      long bytes = (high - (low < 0 ? 0 : low)) + 1;
      logger.trace("Using size-limiting stream (" + bytes + " bytes)");
      return new SizeLimitInputStream(input, bytes);
    }
    return input;
  }

  public String mimeType() {
    if (getPlayer() != null) {
      // FIXME: This cannot be right. A player like FFmpeg can output many
      // formats depending on the media and the renderer. Also, players are
      // singletons. Therefore it is impossible to have exactly one mime
      // type to return.
      return getPlayer().mimeType();
    } else if (getMedia() != null && getMedia().isMediaparsed()) {
      return getMedia().getMimeType();
    } else if (getFormat() != null) {
      return getFormat().mimeType();
    } else {
      return getDefaultMimeType(getSpecificType());
    }
  }

  /**
   * Prototype function. Original comment: need to override if some thumbnail work is to be done when mediaparserv2 enabled
   */
  public void checkThumbnail() {
    // need to override if some thumbnail work is to be done when mediaparserv2 enabled
  }

  /**
   * Checks if a thumbnail exists, and, if not, generates one (if possible).
   * Called from Request/RequestV2 in response to thumbnail requests e.g. HEAD /get/0$1$0$42$3/thumbnail0000%5BExample.mkv
   * Calls DLNAMediaInfo.generateThumbnail, which in turn calls DLNAMediaInfo.parse.
   *
   * @param inputFile File to check or generate the thumbnail for.
   */
  protected void checkThumbnail(InputFile inputFile) {
    if (getMedia() != null && !getMedia().isThumbready() && configuration.isThumbnailGenerationEnabled()) {
      getMedia().setThumbready(true);
      getMedia().generateThumbnail(inputFile, getFormat(), getType());
      if (getMedia().getThumb() != null && configuration.getUseCache() && inputFile.getFile() != null) {
        PMS.get().getDatabase().updateThumbnail(inputFile.getFile().getAbsolutePath(), inputFile.getFile().lastModified(), getType(), getMedia());
      }
    }
  }

  /**
   * Returns the input stream for this resource's generic thumbnail,
   * which is the first of:
   *          - its Format icon, if any
   *          - the fallback image, if any
   *          - the default video icon
   *
   * @param fallback
   *            the fallback image, or null.
   *
   * @return The InputStream
   * @throws IOException
   */
  public InputStream getGenericThumbnailInputStream(String fallback) throws IOException {
    String thumb = fallback;
    if (getFormat() != null && getFormat().getIcon() != null) {
      thumb = getFormat().getIcon();
    }

    // Thumb could be:
    if (thumb != null) {
      // A local file
      if (new File(thumb).canRead()) {
        return new FileInputStream(thumb);
      }

      // A jar resource
      InputStream is;
      if ((is = getResourceInputStream(thumb)) != null) {
        return is;
      }

      // A URL
      try {
        return downloadAndSend(thumb, true);
      } catch (Exception e) {}
    }

    // Or none of the above
    return getResourceInputStream("images/thumbnail-video-256.png");
  }

  /**
   * Returns the input stream for this resource's thumbnail
   * (or a default image if a thumbnail can't be found).
   * Typically overridden by a subclass.
   *
   * @return The InputStream
   * @throws IOException
   */
  public InputStream getThumbnailInputStream() throws IOException {
    return getGenericThumbnailInputStream(null);
  }

  public String getThumbnailContentType() {
    return HTTPResource.JPEG_TYPEMIME;
  }

  public int getType() {
    if (getFormat() != null) {
      return getFormat().getType();
    } else {
      return Format.UNKNOWN;
    }
  }

  /**
   * Prototype function.
   *
   * @return true if child can be added to other folder.
   * @see #addChild(DLNAResource)
   */
  public abstract boolean isValid();

  public boolean allowScan() {
    return false;
  }

  /**
   * (non-Javadoc)
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    result.append(getClass().getSimpleName());
    result.append(" [id=");
    result.append(getId());
    result.append(", name=");
    result.append(getName());
    result.append(", full path=");
    result.append(getResourceId());
    result.append(", ext=");
    result.append(getFormat());
    result.append(", discovered=");
    result.append(isDiscovered());
    result.append("]");
    return result.toString();
  }

  /**
   * Returns the specific type of resource. Valid types are defined in {@link Format}.
   *
   * @return The specific type
   */
  protected int getSpecificType() {
    return specificType;
  }

  /**
   * Set the specific type of this resource. Valid types are defined in {@link Format}.
   * @param specificType The specific type to set.
   */
  protected void setSpecificType(int specificType) {
    this.specificType = specificType;
  }

  /**
   * Returns the {@link Format} of this resource, which defines its capabilities.
   *
   * @return The format of this resource.
   */
  public Format getFormat() {
    return format;
  }

  /**
   * Sets the {@link Format} of this resource, thereby defining its capabilities.
   *
   * @param format The format to set.
   */
  protected void setFormat(Format format) {
    this.format = format;

    // Set deprecated variable for backwards compatibility
    ext = format;
  }

  /**
   * Returns the {@link DLNAMediaInfo} object for this resource, containing the
   * specifics of this resource, e.g. the duration.
   *
   * @return The object containing detailed information.
   */
  public DLNAMediaInfo getMedia() {
    return media;
  }

  /**
   * Sets the the {@link DLNAMediaInfo} object that contains all specifics for
   * this resource.
   *
   * @param media The object containing detailed information.
   * @since 1.50.0
   */
  protected void setMedia(DLNAMediaInfo media) {
    this.media = media;
  }

  /**
   * Returns the {@link DLNAMediaAudio} object for this resource that contains
   * the audio specifics. A resource can have many audio tracks, this method
   * returns the one that should be played.
   *
   * @return The audio object containing detailed information.
   * @since 1.50.0
   */
  public DLNAMediaAudio getMediaAudio() {
    return media_audio;
  }

  /**
   * Sets the {@link DLNAMediaAudio} object for this resource that contains
   * the audio specifics. A resource can have many audio tracks, this method
   * determines the one that should be played.
   *
   * @param mediaAudio The audio object containing detailed information.
   * @since 1.50.0
   */
  protected void setMediaAudio(DLNAMediaAudio mediaAudio) {
    this.media_audio = mediaAudio;
  }

  /**
   * Returns the {@link DLNAMediaSubtitle} object for this resource that
   * contains the specifics for the subtitles. A resource can have many
   * subtitles, this method returns the one that should be displayed.
   *
   * @return The subtitle object containing detailed information.
   * @since 1.50.0
   */
  public DLNAMediaSubtitle getMediaSubtitle() {
    return media_subtitle;
  }

  /**
   * Sets the {@link DLNAMediaSubtitle} object for this resource that
   * contains the specifics for the subtitles. A resource can have many
   * subtitles, this method determines the one that should be used.
   *
   * @param mediaSubtitle The subtitle object containing detailed information.
   * @since 1.50.0
   */
  protected void setMediaSubtitle(DLNAMediaSubtitle mediaSubtitle) {
    this.media_subtitle = mediaSubtitle;
  }

  /**
   * @deprecated Use {@link #getLastModified()} instead.
   *
   * Returns the timestamp at which this resource was last modified.
   *
   * @return The timestamp.
   */
  @Deprecated
  public long getLastmodified() {
    return getLastModified();
  }

  /**
   * Returns the timestamp at which this resource was last modified.
   *
   * @return The timestamp.
   * @since 1.71.0
   */
  public long getLastModified() {
    return lastmodified; // TODO rename lastmodified -> lastModified
  }

  /**
   * @deprecated Use {@link #setLastModified(long)} instead.
   *
   * Sets the timestamp at which this resource was last modified.
   *
   * @param lastModified The timestamp to set.
   * @since 1.50.0
   */
  @Deprecated
  protected void setLastmodified(long lastModified) {
    setLastModified(lastModified);
  }

  /**
   * Sets the timestamp at which this resource was last modified.
   *
   * @param lastModified The timestamp to set.
   * @since 1.71.0
   */
  protected void setLastModified(long lastModified) {
    this.lastmodified = lastModified; // TODO rename lastmodified -> lastModified
  }

  /**
   * Returns the {@link Player} object that is used to encode this resource
   * for the renderer. Can be null.
   *
   * @return The player object.
   */
  public Player getPlayer() {
    return player;
  }

  /**
   * Sets the {@link Player} object that is to be used to encode this
   * resource for the renderer. The player object can be null.
   *
   * @param player The player object to set.
   * @since 1.50.0
   */
  protected void setPlayer(Player player) {
    this.player = player;
  }

  /**
   * Returns true when the details of this resource have already been
   * investigated. This helps is not doing the same work twice.
   *
   * @return True if discovered, false otherwise.
   */
  public boolean isDiscovered() {
    return discovered;
  }

  /**
   * Set to true when the details of this resource have already been
   * investigated. This helps is not doing the same work twice.
   *
   * @param discovered Set to true if this resource is discovered,
   *       false otherwise.
   * @since 1.50.0
   */
  protected void setDiscovered(boolean discovered) {
    this.discovered = discovered;
  }

  /**
   * Returns true if this resource has subtitles in a file.
   *
   * @return the srtFile
   * @since 1.50.0
   */
  protected boolean isSrtFile() {
    return srtFile;
  }

  /**
   * Set to true if this resource has subtitles in a file.
   *
   * @param srtFile the srtFile to set
   * @since 1.50.0
   */
  protected void setSrtFile(boolean srtFile) {
    this.srtFile = srtFile;
  }

  /**
   * Returns the update counter for this resource. When the resource needs
   * to be refreshed, its counter is updated.
   *
   * @return The update counter.
   * @see #notifyRefresh()
   */
  public int getUpdateId() {
    return updateId;
  }

  /**
   * Sets the update counter for this resource. When the resource needs
   * to be refreshed, its counter should be updated.
   *
   * @param updateId The counter value to set.
   * @since 1.50.0
   */
  protected void setUpdateId(int updateId) {
    this.updateId = updateId;
  }

  /**
   * Returns the update counter for all resources. When all resources need
   * to be refreshed, this counter is updated.
   *
   * @return The system update counter.
   * @since 1.50.0
   */
  public static int getSystemUpdateId() {
    return systemUpdateId;
  }

  /**
   * Sets the update counter for all resources. When all resources need
   * to be refreshed, this counter should be updated.
   *
   * @param systemUpdateId The system update counter to set.
   * @since 1.50.0
   */
  public static void setSystemUpdateId(int systemUpdateId) {
    DLNAResource.systemUpdateId = systemUpdateId;
  }

  /**
   * Returns whether or not this is a nameless resource.
   *
   * @return True if the resource is nameless.
   */
  public boolean isNoName() {
    return noName;
  }

  /**
   * Sets whether or not this is a nameless resource. This is particularly
   * useful in the virtual TRANSCODE folder for a file, where the same file
   * is copied many times with different audio and subtitle settings. In that
   * case the name of the file becomes irrelevant and only the settings
   * need to be shown.
   *
   * @param noName Set to true if the resource is nameless.
   * @since 1.50.0
   */
  protected void setNoName(boolean noName) {
    this.noName = noName;
  }

  /**
   * Returns the from - to time range for this resource.
   *
   * @return The time range.
   */
  public Range.Time getSplitRange() {
    return splitRange;
  }

  /**
   * Sets the from - to time range for this resource.
   *
   * @param splitRange The time range to set.
   * @since 1.50.0
   */
  protected void setSplitRange(Range.Time splitRange) {
    this.splitRange = splitRange;
  }

  /**
   * Returns the number of the track to split from this resource.
   *
   * @return the splitTrack
   * @since 1.50.0
   */
  protected int getSplitTrack() {
    return splitTrack;
  }

  /**
   * Sets the number of the track from this resource to split.
   *
   * @param splitTrack The track number.
   * @since 1.50.0
   */
  protected void setSplitTrack(int splitTrack) {
    this.splitTrack = splitTrack;
  }

  /**
   * Returns the default renderer configuration for this resource.
   *
   * @return The default renderer configuration.
   * @since 1.50.0
   */
  protected RendererConfiguration getDefaultRenderer() {
    return defaultRenderer;
  }

  /**
   * Sets the default renderer configuration for this resource.
   *
   * @param defaultRenderer The default renderer configuration to set.
   * @since 1.50.0
   */
  protected void setDefaultRenderer(RendererConfiguration defaultRenderer) {
    this.defaultRenderer = defaultRenderer;
  }

  /**
   * Returns whether or not this resource is handled by AviSynth.
   *
   * @return True if handled by AviSynth, otherwise false.
   * @since 1.50.0
   */
  protected boolean isAvisynth() {
    return avisynth;
  }

  /**
   * Sets whether or not this resource is handled by AviSynth.
   *
   * @param avisynth Set to true if handled by Avisyth, otherwise false.
   * @since 1.50.0
   */
  protected void setAvisynth(boolean avisynth) {
    this.avisynth = avisynth;
  }

  /**
   * Returns true if transcoding should be skipped for this resource.
   *
   * @return True if transcoding should be skipped, false otherwise.
   * @since 1.50.0
   */
  protected boolean isSkipTranscode() {
    return skipTranscode;
  }

  /**
   * Set to true if transcoding should be skipped for this resource.
   *
   * @param skipTranscode Set to true if trancoding should be skipped, false
   *       otherwise.
   * @since 1.50.0
   */
  protected void setSkipTranscode(boolean skipTranscode) {
    this.skipTranscode = skipTranscode;
  }

  /**
   * Returns the list of children for this resource.
   *
   * @return List of children objects.
   */
  public List<DLNAResource> getChildren() {
    return children;
  }

  /**
   * Sets the list of children for this resource.
   *
   * @param children The list of children to set.
   * @since 1.50.0
   */
  protected void setChildren(List<DLNAResource> children) {
    this.children = children;
  }

  /**
   * @deprecated use {@link #getLastChildId()} instead.
   */
  @Deprecated
  protected int getLastChildrenId() {
    return getLastChildId();
  }

  /**
   * Returns the numerical ID of the last child added.
   *
   * @return The ID.
   * @since 1.80.0
   */
  protected int getLastChildId() {
    return lastChildrenId;
  }

  /**
   * @deprecated use {@link #setLastChildId(int)} instead.
   */
  @Deprecated
  protected void setLastChildrenId(int lastChildId) {
    setLastChildId(lastChildId);
  }

  /**
   * Sets the numerical ID of the last child added.
   *
   * @param lastChildId The ID to set.
   * @since 1.80.0
   */
  protected void setLastChildId(int lastChildId) {
    this.lastChildrenId = lastChildId;
  }

  /**
   * Returns the timestamp when this resource was last refreshed.
   *
   * @return The timestamp.
   */
  long getLastRefreshTime() {
    return lastRefreshTime;
  }

  /**
   * Sets the timestamp when this resource was last refreshed.
   *
   * @param lastRefreshTime The timestamp to set.
   * @since 1.50.0
   */
  protected void setLastRefreshTime(long lastRefreshTime) {
    this.lastRefreshTime = lastRefreshTime;
  }
}
TOP

Related Classes of net.pms.dlna.DLNAResource

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.