/*
* 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 java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import net.pms.configuration.MapFileConfiguration;
import net.pms.util.FileUtil;
import net.pms.util.UMSUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 List<File> discoverable;
private List<File> emptyFoldersToRescan;
private String forcedName;
private ArrayList<RealFile> searchList;
/**
* @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;
public MapFile() {
this.conf = new MapFileConfiguration();
setLastModified(0);
forcedName = null;
}
public MapFile(MapFileConfiguration conf) {
this.conf = conf;
setLastModified(0);
forcedName = null;
}
public MapFile(MapFileConfiguration conf, List<File> list) {
this.conf = conf;
setLastModified(0);
this.discoverable = list;
forcedName = null;
}
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 (configuration.isArchiveBrowsing() && (lcFilename.endsWith(".tar") || lcFilename.endsWith(".gzip") || lcFilename.endsWith(".gz") || lcFilename.endsWith(".7z"))) {
addChild(new SevenZipFile(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") || lcFilename.endsWith(".cue") || lcFilename.endsWith(".ups")) {
DLNAResource d = PlaylistFolder.getPlaylist(lcFilename, f.getAbsolutePath(), 0);
if (d != null) {
addChild(d);
}
} else {
/* Optionally ignore empty directories */
if (f.isDirectory() && configuration.isHideEmptyFolders() && !FileUtil.isFolderRelevant(f, configuration)) {
LOGGER.debug("Ignoring empty/non-relevant directory: " + f.getName());
// Keep track of the fact that we have empty folders, so when we're asked if we should refresh,
// we can re-scan the folders in this list to see if they contain something relevant
if (emptyFoldersToRescan == null) {
emptyFoldersToRescan = new ArrayList<>();
}
if (!emptyFoldersToRescan.contains(f)) {
emptyFoldersToRescan.add(f);
}
} else { // Otherwise add the file
RealFile rf = new RealFile(f);
if (searchList != null) {
searchList.add(rf);
}
addChild(rf);
}
}
}
if (f.isFile() && (lcFilename.equals("folder.jpg") || lcFilename.equals("folder.png") || (lcFilename.contains("albumart") && lcFilename.endsWith(".jpg")))) {
potentialCover = f;
}
}
}
private List<File> getFileList() {
List<File> out = new ArrayList<>();
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;
FileSearch fs = null;
if (!discoverable.isEmpty() && configuration.getSearchInFolder()) {
searchList = new ArrayList<>();
fs = new FileSearch(searchList);
addChild(new SearchFolder(fs));
}
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));
}
}
if (fs != null) {
fs.update(searchList);
}
return discoverable.isEmpty();
}
@Override
public void discoverChildren() {
discoverChildren(null);
}
@Override
public void discoverChildren(String str) {
if (discoverable == null) {
discoverable = new ArrayList<>();
} else {
return;
}
int sm = configuration.getSortMethod(getPath());
List<File> files = getFileList();
// ATZ handling
if (files.size() > configuration.getATZLimit() && StringUtils.isEmpty(forcedName)) {
/*
* Too many files to display at once, add A-Z folders
* instead and let the filters begin
*
* Note: If we done this at the level directly above we don't do it again
* since all files start with the same letter then
*/
TreeMap<String, ArrayList<File>> map = new TreeMap<>();
for (File f : files) {
if ((!f.isFile() && !f.isDirectory()) || f.isHidden()) {
// skip these
continue;
}
if (f.isDirectory() && configuration.isHideEmptyFolders() && !FileUtil.isFolderRelevant(f, configuration)) {
LOGGER.debug("Ignoring empty/non-relevant directory: " + f.getName());
// Keep track of the fact that we have empty folders, so when we're asked if we should refresh,
// we can re-scan the folders in this list to see if they contain something relevant
if (emptyFoldersToRescan == null) {
emptyFoldersToRescan = new ArrayList<>();
}
if (!emptyFoldersToRescan.contains(f)) {
emptyFoldersToRescan.add(f);
}
continue;
}
String filenameToSort = FileUtil.renameForSorting(f.getName());
char c = filenameToSort.toUpperCase().charAt(0);
if (!(c >= 'A' && c <= 'Z')) {
// "other char"
c = '#';
}
ArrayList<File> l = map.get(String.valueOf(c));
if (l == null) {
// new letter
l = new ArrayList<>();
}
l.add(f);
map.put(String.valueOf(c), l);
}
for (String letter : map.keySet()) {
// loop over all letters, this avoids adding
// empty letters
ArrayList<File> l = map.get(letter);
UMSUtils.sort(l, sm);
MapFile mf = new MapFile(getConf(), l);
mf.forcedName = letter;
addChild(mf);
}
return;
}
UMSUtils.sort(files, (sm == UMSUtils.SORT_RANDOM ? UMSUtils.SORT_LOC_NAT : sm));
for (File f : files) {
if (f.isDirectory()) {
discoverable.add(f); // manageFile(f);
}
}
// For random sorting, we only randomize file entries
if (sm == UMSUtils.SORT_RANDOM) {
UMSUtils.sort(files, sm);
}
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());
}
}
// Check if any of our previously empty folders now have content
boolean emptyFolderNowNotEmpty = false;
if (emptyFoldersToRescan != null) {
for (File emptyFile : emptyFoldersToRescan) {
if (FileUtil.isFolderRelevant(emptyFile, configuration)) {
emptyFolderNowNotEmpty = true;
break;
}
}
}
return (getLastRefreshTime() < modified) || (configuration.getSortMethod(getPath()) == UMSUtils.SORT_RANDOM || emptyFolderNowNotEmpty);
}
@Override
public void doRefreshChildren() {
doRefreshChildren(null);
}
@Override
public void doRefreshChildren(String str) {
getChildren().clear();
emptyFoldersToRescan = null; // Since we're re-scanning, reset this list so it can be built again
discoverable = null;
discoverChildren(str);
analyzeChildren(-1);
}
@Override
public String getSystemName() {
return getName();
}
@Override
public long length() {
return 0;
}
@Override
public String getName() {
if (StringUtils.isEmpty(forcedName)) {
return this.getConf().getName();
}
return forcedName;
}
@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
*/
protected MapFileConfiguration getConf() {
return conf;
}
/**
* @param conf the conf to set
* @since 1.50
*/
protected void setConf(MapFileConfiguration conf) {
this.conf = conf;
}
/**
* @return the potentialCover
* @since 1.50
*/
public File getPotentialCover() {
return potentialCover;
}
/**
* @param potentialCover the potentialCover to set
* @since 1.50
*/
public void setPotentialCover(File potentialCover) {
this.potentialCover = potentialCover;
}
@Override
public boolean isSearched() {
return (getParent() instanceof SearchFolder);
}
private File getPath() {
if (this instanceof RealFile) {
return ((RealFile) this).getFile();
}
return null;
}
}