/*
* 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 net.pms.PMS;
import net.pms.configuration.MapFileConfiguration;
import net.pms.configuration.PmsConfiguration;
import net.pms.dlna.virtual.TranscodeVirtualFolder;
import net.pms.dlna.virtual.VirtualFolder;
import net.pms.formats.FormatFactory;
import net.pms.network.HTTPResource;
import net.pms.util.NaturalComparator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.Collator;
import java.util.*;
/**
* 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 class MapFile extends DLNAResource {
private static final Logger logger = LoggerFactory.getLogger(MapFile.class);
private static final PmsConfiguration configuration = PMS.getConfiguration();
private List<File> discoverable;
/**
* @deprecated Use standard getter and setter to access this variable.
*/
@Deprecated
public File potentialCover;
/**
* @deprecated Use standard getter and setter to access this variable.
*/
@Deprecated
protected MapFileConfiguration conf;
private static final Collator collator;
static {
collator = Collator.getInstance();
collator.setStrength(Collator.PRIMARY);
}
public MapFile() {
setConf(new MapFileConfiguration());
setLastModified(0);
}
public MapFile(MapFileConfiguration conf) {
setConf(conf);
setLastModified(0);
}
private boolean isFileRelevant(File f) {
String fileName = f.getName().toLowerCase();
return (configuration.isArchiveBrowsing() && (fileName.endsWith(".zip") || fileName.endsWith(".cbz")
|| fileName.endsWith(".rar") || fileName.endsWith(".cbr")))
|| fileName.endsWith(".iso") || fileName.endsWith(".img")
|| fileName.endsWith(".m3u") || fileName.endsWith(".m3u8") || fileName.endsWith(".pls") || fileName.endsWith(".cue");
}
private boolean isFolderRelevant(File f) {
boolean isRelevant = false;
if (f.isDirectory() && configuration.isHideEmptyFolders()) {
File[] children = f.listFiles();
// listFiles() returns null if "this abstract pathname does not denote a directory, or if an I/O error occurs".
// in this case (since we've already confirmed that it's a directory), this seems to mean the directory is non-readable
// http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=15135
// http://stackoverflow.com/questions/3228147/retrieving-the-underlying-error-when-file-listfiles-return-null
if (children == null) {
logger.warn("Can't list files in non-readable directory: {}", f.getAbsolutePath());
} else {
for (File child : children) {
if (child.isFile()) {
if (FormatFactory.getAssociatedFormat(child.getName()) != null || isFileRelevant(child)) {
isRelevant = true;
break;
}
} else {
if (isFolderRelevant(child)) {
isRelevant = true;
break;
}
}
}
}
}
return isRelevant;
}
private void manageFile(File f) {
if (f.isFile() || f.isDirectory()) {
String lcFilename = f.getName().toLowerCase();
if (!f.isHidden()) {
if (configuration.isArchiveBrowsing() && (lcFilename.endsWith(".zip") || lcFilename.endsWith(".cbz"))) {
addChild(new ZippedFile(f));
} else if (configuration.isArchiveBrowsing() && (lcFilename.endsWith(".rar") || lcFilename.endsWith(".cbr"))) {
addChild(new RarredFile(f));
} else if ((lcFilename.endsWith(".iso") || lcFilename.endsWith(".img")) || (f.isDirectory() && f.getName().toUpperCase().equals("VIDEO_TS"))) {
addChild(new DVDISOFile(f));
} else if (lcFilename.endsWith(".m3u") || lcFilename.endsWith(".m3u8") || lcFilename.endsWith(".pls")) {
addChild(new PlaylistFolder(f));
} else if (lcFilename.endsWith(".cue")) {
addChild(new CueFolder(f));
} else {
/* Optionally ignore empty directories */
if (f.isDirectory() && configuration.isHideEmptyFolders() && !isFolderRelevant(f)) {
logger.debug("Ignoring empty/non-relevant directory: " + f.getName());
} else { // Otherwise add the file
addChild(new RealFile(f));
}
}
}
// FIXME this causes folder thumbnails to take precedence over file thumbnails
if (f.isFile()) {
if (lcFilename.equals("folder.jpg") || lcFilename.equals("folder.png") || (lcFilename.contains("albumart") && lcFilename.endsWith(".jpg"))) {
setPotentialCover(f);
}
}
}
}
private List<File> getFileList() {
List<File> out = new ArrayList<File>();
for (File file : this.conf.getFiles()) {
if (file != null && file.isDirectory()) {
if (file.canRead()) {
File[] files = file.listFiles();
if (files == null) {
logger.warn("Can't read files from directory: {}", file.getAbsolutePath());
} else {
out.addAll(Arrays.asList(files));
}
} else {
logger.warn("Can't read directory: {}", file.getAbsolutePath());
}
}
}
return out;
}
@Override
public boolean isValid() {
return true;
}
@Override
public boolean analyzeChildren(int count) {
int currentChildrenCount = getChildren().size();
int vfolder = 0;
while (((getChildren().size() - currentChildrenCount) < count) || (count == -1)) {
if (vfolder < getConf().getChildren().size()) {
addChild(new MapFile(getConf().getChildren().get(vfolder)));
++vfolder;
} else {
if (discoverable.isEmpty()) {
break;
}
manageFile(discoverable.remove(0));
}
}
return discoverable.isEmpty();
}
@Override
public void discoverChildren() {
super.discoverChildren();
if (discoverable == null) {
discoverable = new ArrayList<File>();
} else {
return;
}
List<File> files = getFileList();
switch (configuration.getSortMethod()) {
case 4: // Locale-sensitive natural sort
Collections.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return NaturalComparator.compareNatural(collator, f1.getName(), f2.getName());
}
});
break;
case 3: // Case-insensitive ASCIIbetical sort
Collections.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
return f1.getName().compareToIgnoreCase(f2.getName());
}
});
break;
case 2: // Sort by modified date, oldest first
Collections.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return Long.valueOf(f1.lastModified()).compareTo(Long.valueOf(f2.lastModified()));
}
});
break;
case 1: // Sort by modified date, newest first
Collections.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return Long.valueOf(f2.lastModified()).compareTo(Long.valueOf(f1.lastModified()));
}
});
break;
default: // Locale-sensitive A-Z
Collections.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
return collator.compare(f1.getName(), f2.getName());
}
});
break;
}
for (File f : files) {
if (f.isDirectory()) {
discoverable.add(f); // manageFile(f);
}
}
// We only randomize file entries, directories are still sorted alphabetically
if (configuration.getSortMethod() == 5) {
Collections.shuffle(files);
}
for (File f : files) {
if (f.isFile()) {
discoverable.add(f); // manageFile(f);
}
}
}
@Override
public boolean isRefreshNeeded() {
long modified = 0;
for (File f : this.getConf().getFiles()) {
if (f != null) {
modified = Math.max(modified, f.lastModified());
}
}
return getLastRefreshTime() < modified;
}
@Override
public void doRefreshChildren() {
List<File> files = getFileList();
List<File> addedFiles = new ArrayList<File>();
List<DLNAResource> removedFiles = new ArrayList<DLNAResource>();
for (DLNAResource d : getChildren()) {
boolean isNeedMatching = !(d.getClass() == MapFile.class || (d instanceof VirtualFolder && !(d instanceof DVDISOFile)));
if (isNeedMatching && !foundInList(files, d)) {
removedFiles.add(d);
}
}
for (File f : files) {
if (!f.isHidden() && (f.isDirectory() || FormatFactory.getAssociatedFormat(f.getName()) != null)) {
addedFiles.add(f);
}
}
for (DLNAResource f : removedFiles) {
logger.debug("File automatically removed: " + f.getName());
}
for (File f : addedFiles) {
logger.debug("File automatically added: " + f.getName());
}
// false: don't create the folder if it doesn't exist i.e. find the folder
TranscodeVirtualFolder transcodeFolder = getTranscodeFolder(false);
for (DLNAResource f : removedFiles) {
getChildren().remove(f);
if (transcodeFolder != null) {
for (int j = transcodeFolder.getChildren().size() - 1; j >= 0; j--) {
if (transcodeFolder.getChildren().get(j).getName().equals(f.getName())) {
transcodeFolder.getChildren().remove(j);
}
}
}
}
for (File f : addedFiles) {
manageFile(f);
}
for (MapFileConfiguration f : this.getConf().getChildren()) {
addChild(new MapFile(f));
}
}
private boolean foundInList(List<File> files, DLNAResource dlna) {
for (File file: files) {
if (!file.isHidden() && isNameMatch(dlna, file) && (isRealFolder(dlna) || isSameLastModified(dlna, file))) {
files.remove(file);
return true;
}
}
return false;
}
private boolean isSameLastModified(DLNAResource dlna, File file) {
return dlna.getLastModified() == file.lastModified();
}
private boolean isRealFolder(DLNAResource dlna) {
return dlna instanceof RealFile && dlna.isFolder();
}
private boolean isNameMatch(DLNAResource dlna, File file) {
return (dlna.getName().equals(file.getName()) || isDVDIsoMatch(dlna, file));
}
private boolean isDVDIsoMatch(DLNAResource dlna, File file) {
if (dlna instanceof DVDISOFile) {
DVDISOFile dvdISOFile = (DVDISOFile) dlna;
return dvdISOFile.getFilename().equals(file.getName());
} else {
return false;
}
}
@Override
public String getSystemName() {
return getName();
}
@Override
public String getThumbnailContentType() {
String thumbnailIcon = this.getConf().getThumbnailIcon();
if (thumbnailIcon != null && thumbnailIcon.toLowerCase().endsWith(".png")) {
return HTTPResource.PNG_TYPEMIME;
}
return super.getThumbnailContentType();
}
@Override
public InputStream getThumbnailInputStream() throws IOException {
return this.getConf().getThumbnailIcon() != null
? getResourceInputStream(this.getConf().getThumbnailIcon())
: super.getThumbnailInputStream();
}
@Override
public long length() {
return 0;
}
@Override
public String getName() {
return this.getConf().getName();
}
@Override
public boolean isFolder() {
return true;
}
@Override
public InputStream getInputStream() throws IOException {
return null;
}
@Override
public boolean allowScan() {
return isFolder();
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "MapFile [name=" + getName() + ", id=" + getResourceId() + ", format=" + getFormat() + ", children=" + getChildren() + "]";
}
/**
* @return the conf
* @since 1.50.0
*/
protected MapFileConfiguration getConf() {
return conf;
}
/**
* @param conf the conf to set
* @since 1.50.0
*/
protected void setConf(MapFileConfiguration conf) {
this.conf = conf;
}
/**
* @return the potentialCover
* @since 1.50.0
*/
public File getPotentialCover() {
return potentialCover;
}
/**
* @param potentialCover the potentialCover to set
* @since 1.50.0
*/
public void setPotentialCover(File potentialCover) {
this.potentialCover = potentialCover;
}
}