/*
* 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 &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 &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 -> 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;
}
}