package net.pms.dlna; import com.sun.jna.Platform; import com.sun.jna.platform.FileUtils; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashSet; import java.util.Set; import net.pms.Messages; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.dlna.virtual.VirtualFolder; import net.pms.dlna.virtual.VirtualVideoAction; import net.pms.util.FileUtil; import net.pms.util.FreedesktopTrash; import net.pms.util.FullyPlayedAction; import org.apache.commons.lang.StringUtils; import org.fest.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MediaMonitor extends VirtualFolder { private static Set<String> fullyPlayedEntries; private File[] dirs; private PmsConfiguration config; private static final Logger LOGGER = LoggerFactory.getLogger(MediaMonitor.class); public MediaMonitor(File[] dirs) { super(Messages.getString("VirtualFolder.2"), "images/thumbnail-folder-256.png"); this.dirs = dirs; fullyPlayedEntries = new HashSet<>(); config = PMS.getConfiguration(); parseMonitorFile(); } /** * The UTF-8 encoded file containing fully played entries. * @return The file */ private File monitorFile() { return new File(config.getDataFile("UMS.mon")); } private void parseMonitorFile() { File f = monitorFile(); if (!f.exists()) { return; } try { try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) { String str; while ((str = in.readLine()) != null) { if (StringUtils.isEmpty(str)) { continue; } str = str.trim(); if (str.startsWith("#")) { continue; } if (str.startsWith("entry=")) { String entry = str.substring(6); if (!new File(entry.trim()).exists()) { continue; } fullyPlayedEntries.add(entry.trim()); } } } dumpFile(); } catch (IOException e) { } } public void scanDir(File[] files, final DLNAResource res) { if (files != null) { final DLNAResource mm = this; res.addChild(new VirtualVideoAction(Messages.getString("PMS.150"), true) { @Override public boolean enable() { for (DLNAResource r : res.getChildren()) { if (!(r instanceof RealFile)) { continue; } RealFile rf = (RealFile) r; fullyPlayedEntries.add(rf.getFile().getAbsolutePath()); } mm.setDiscovered(false); mm.getChildren().clear(); try { dumpFile(); } catch (IOException e) { } return true; } }); for (File f : files) { if (f.isFile()) { if (isFullyPlayed(f.getAbsolutePath())) { continue; } res.addChild(new RealFile(f)); } if (f.isDirectory()) { boolean add = true; if (config.isHideEmptyFolders()) { add = FileUtil.isFolderRelevant(f, config, fullyPlayedEntries); } if (add) { res.addChild(new MonitorEntry(f, this)); } } } } } @Override public void discoverChildren() { if (dirs != null) { for (File f : dirs) { scanDir(f.listFiles(), this); } } } @Override public boolean isRefreshNeeded() { return true; } public void stopped(DLNAResource res) { if (!(res instanceof RealFile)) { return; } RealFile rf = (RealFile) res; // The total video duration in seconds double fileDuration = 0; if (res.getMedia() != null && (res.getMedia().isAudio() || res.getMedia().isVideo())) { fileDuration = res.getMedia().getDurationInSeconds(); } /** * Time since the file started playing. * This is not a great way to get this value because if the * video is paused, it will no longer be accurate. */ double elapsed; if (res.getLastStartPosition() == 0) { elapsed = (double) (System.currentTimeMillis() - res.getStartTime()) / 1000; } else { elapsed = (double) (System.currentTimeMillis() - res.getLastStartSystemTime()) / 1000; elapsed += res.getLastStartPosition(); } FullyPlayedAction fullyPlayedAction = configuration.getFullyPlayedAction(); if (!fullyPlayedAction.equals(FullyPlayedAction.NO_ACTION)) { LOGGER.trace("Fully Played feature logging:"); LOGGER.trace(" duration: " + fileDuration); LOGGER.trace(" getLastStartPosition: " + res.getLastStartPosition()); LOGGER.trace(" getStartTime: " + res.getStartTime()); LOGGER.trace(" getLastStartSystemTime: " + res.getLastStartSystemTime()); LOGGER.trace(" elapsed: " + elapsed); LOGGER.trace(" minimum play time needed: " + (fileDuration * configuration.getResumeBackFactor())); } /** * Only mark the file as fully played if more than 92% (default) of * the duration has elapsed since it started playing. */ if ( ( res.getMedia() != null && res.getMedia().isImage() ) || ( fileDuration > configuration.getMinimumWatchedPlayTimeSeconds() && elapsed >= (fileDuration * configuration.getResumeBackFactor()) ) ) { DLNAResource tmp = res.getParent(); if (tmp != null) { boolean isMonitored = false; File[] foldersMonitored = PMS.get().getSharedFoldersArray(true); if (foldersMonitored != null && foldersMonitored.length > 0) { for (File folderMonitored : foldersMonitored) { if (rf.getFile().getAbsolutePath().contains(folderMonitored.getAbsolutePath())) { isMonitored = true; } } } if (isMonitored) { // Prevent duplicates from being added if (isFullyPlayed(rf.getFile().getAbsolutePath())) { return; } fullyPlayedEntries.add(rf.getFile().getAbsolutePath()); setDiscovered(false); getChildren().clear(); try { dumpFile(); } catch (IOException e) { LOGGER.debug("An error occurred when dumping monitor file: " + e); } File playedFile = new File(rf.getFile().getAbsolutePath()); if (fullyPlayedAction == FullyPlayedAction.MOVE_FOLDER) { // Move the video to a different folder String newDirectory = FileUtil.appendPathSeparator(configuration.getFullyPlayedOutputDirectory()); if (playedFile.renameTo(new File(newDirectory + playedFile.getName()))) { LOGGER.debug("Moved {} because it has been fully played", playedFile.getName()); } else { LOGGER.debug("Moving {} failed, trying again in 3 seconds", playedFile.getName()); try { Thread.sleep(3000); if (playedFile.renameTo(new File(newDirectory + playedFile.getName()))) { LOGGER.debug("Moved {} because it has been fully played", playedFile.getName()); } else { LOGGER.info("Failed to move {}", playedFile.getName()); } } catch (InterruptedException e) { LOGGER.warn("Abandoning moving of {} because the thread was interrupted, probably due to UMS shutdown", e.getMessage()); LOGGER.trace("", e); Thread.currentThread().interrupt(); } } } else if (fullyPlayedAction == FullyPlayedAction.MOVE_TRASH) { try { if (Platform.isLinux()) { FreedesktopTrash.moveToTrash(playedFile); } else { FileUtils.getInstance().moveToTrash(Arrays.array(playedFile)); } } catch (IOException | FileUtil.InvalidFileSystemException e) { LOGGER.warn("Failed to move file \"{}\" to recycler/trash after it has been fully played: {}", playedFile.getAbsoluteFile(), e.getMessage()); LOGGER.trace("", e); } } LOGGER.info("{} marked as fully played", playedFile.getName()); } } } else { LOGGER.trace(" final decision: not fully played"); } } public static boolean isFullyPlayed(String str) { return fullyPlayedEntries != null && fullyPlayedEntries.contains(str); } /** * Populates UMS.mon with a list of completely played media. * * @throws IOException */ private void dumpFile() throws IOException { File f = monitorFile(); Date now = new Date(); try (OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) { StringBuilder sb = new StringBuilder(); sb.append("######\n"); sb.append("## NOTE!!!!!\n"); sb.append("## This file is auto generated\n"); sb.append("## Edit with EXTREME care\n"); sb.append("## Generated: "); sb.append(now.toString()); sb.append("\n"); for (String str : fullyPlayedEntries) { if (sb.indexOf(str) == -1) { sb.append("entry="); sb.append(str); sb.append("\n"); } } out.write(sb.toString()); out.flush(); } } @Override public void doRefreshChildren() { setUpdateId(this.getIntId()); } }