From fae09a653abc5a53d01a036ade177aa07bd6f483 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Thu, 1 Dec 2011 17:06:51 +0000 Subject: [PATCH] * added WatchService to Scripting API --- .../filebot/cli/FolderWatchService.java | 206 ++++++++++++++++++ .../sourceforge/filebot/cli/ScriptShell.java | 1 + .../filebot/cli/ScriptShell.lib.groovy | 31 +++ website/data/shell/sorty.groovy | 7 +- website/data/shell/watcher.groovy | 8 + 5 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 source/net/sourceforge/filebot/cli/FolderWatchService.java create mode 100644 website/data/shell/watcher.groovy diff --git a/source/net/sourceforge/filebot/cli/FolderWatchService.java b/source/net/sourceforge/filebot/cli/FolderWatchService.java new file mode 100644 index 00000000..1c582d8f --- /dev/null +++ b/source/net/sourceforge/filebot/cli/FolderWatchService.java @@ -0,0 +1,206 @@ + +package net.sourceforge.filebot.cli; + + +import static java.nio.file.StandardWatchEventKinds.*; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.sourceforge.tuned.DefaultThreadFactory; +import net.sourceforge.tuned.Timer; + + +public abstract class FolderWatchService implements Closeable { + + private final Collection commitSet = new HashSet(); + + private final ExecutorService processor = Executors.newSingleThreadExecutor(); + private final ExecutorService watchers = Executors.newCachedThreadPool(new DefaultThreadFactory("FolderWatchService", Thread.MIN_PRIORITY, true)); + + private long commitDelay = 500; // 0.5 s + private final Timer commitTimer = new Timer() { + + @Override + public void run() { + synchronized (processor) { + commit(); + } + } + }; + + + public synchronized void setCommitDelay(long commitDelay) { + if (commitDelay < 0) + throw new IllegalArgumentException("Delay must not be negativ"); + + this.commitDelay = commitDelay; + resetCommitTimer(); + } + + + public synchronized void resetCommitTimer() { + commitTimer.set(commitDelay, TimeUnit.MILLISECONDS, false); + } + + + public synchronized void commit() { + final SortedSet files = new TreeSet(); + + synchronized (commitSet) { + for (Path path : commitSet) { + files.add(path.toFile()); + } + commitSet.clear(); + } + + if (files.isEmpty()) { + return; + } + + processor.submit(new Runnable() { + + @Override + public void run() { + synchronized (processor) { + processCommitSet(files.toArray(new File[0])); + } + } + }); + } + + + public abstract void processCommitSet(File[] files); + + + public synchronized void watch(File node) throws IOException { + if (!node.isDirectory()) { + throw new IllegalArgumentException("Must be a folder: " + node); + } + + watchers.submit(new FolderWatcher(node.toPath()) { + + @Override + protected void processEvents(List> events) { + synchronized (commitSet) { + resetCommitTimer(); + super.processEvents(events); + } + } + + + @Override + protected void created(Path path) { + commitSet.add(path); + } + + + @Override + protected void modified(Path path) { + commitSet.add(path); + } + + + @Override + protected void deleted(Path path) { + commitSet.remove(path); + } + }); + } + + + @Override + public synchronized void close() throws IOException { + commitTimer.cancel(); + processor.shutdownNow(); + watchers.shutdownNow(); + } + + + private abstract static class FolderWatcher implements Runnable, Closeable { + + private final Path node; + private final WatchService watchService; + + + public FolderWatcher(Path node) throws IOException { + this.node = node; + this.watchService = node.getFileSystem().newWatchService(); + node.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); + } + + + @Override + public void run() { + try { + watch(); + } catch (InterruptedException e) { + // ignore, part of an orderly shutdown + } catch (Exception e) { + Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e); + } + } + + + public void watch() throws IOException, InterruptedException { + try { + while (true) { + WatchKey key = watchService.take(); + processEvents(key.pollEvents()); + key.reset(); + } + } finally { + this.close(); + } + } + + + public Path getPath(WatchEvent event) { + return node.resolve(event.context().toString()); + } + + + protected void processEvents(List> list) { + for (WatchEvent event : list) { + if (event.kind() == ENTRY_CREATE) { + created(getPath(event)); + } else if (event.kind() == ENTRY_MODIFY) { + modified(getPath(event)); + } else if (event.kind() == ENTRY_DELETE) { + deleted(getPath(event)); + } + } + } + + + protected abstract void created(Path path); + + + protected abstract void modified(Path path); + + + protected abstract void deleted(Path path); + + + @Override + public void close() throws IOException { + watchService.close(); + } + } + +} diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.java b/source/net/sourceforge/filebot/cli/ScriptShell.java index 39fadb0b..2647c69a 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.java +++ b/source/net/sourceforge/filebot/cli/ScriptShell.java @@ -67,6 +67,7 @@ class ScriptShell { bindings.put(service.getName().toLowerCase(), PrivilegedInvocation.newProxy(MovieIdentificationService.class, service, acc)); } + bindings.put("console", System.console()); return bindings; } diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index 6cbff0b5..92c6aec6 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -39,6 +39,7 @@ File.metaClass.moveTo = { f -> renameFile(delegate, f) } List.metaClass.mapByFolder = { mapByFolder(delegate) } List.metaClass.mapByExtension = { mapByExtension(delegate) } + // Shell helper import static com.sun.jna.Platform.*; @@ -62,6 +63,36 @@ def execute(String... args) { } +// WatchService helper +import net.sourceforge.filebot.cli.FolderWatchService; + +def getWatchService(Closure callback, List folders) { + // sanity check + folders.find{ if (!it.isDirectory()) throw new Exception("Must be a folder: " + it) } + + // create watch service and setup callback + def watchService = new FolderWatchService() { + + @Override + def void processCommitSet(File[] fileset) { + callback(fileset.toList()) + } + } + + // collect updates for 5 minutes and then batch process + watchService.setCommitDelay(5 * 60 * 1000) + + // start watching given files + folders.each { watchService.watch(it) } + + return watchService +} + +File.metaClass.watch = { c -> getWatchService(c, [delegate]) } +List.metaClass.watch = { c -> getWatchService(c, delegate) } + + + // CLI bindings def rename(args) { args = _defaults(args) _guarded { _cli.rename(_files(args), args.query, args.format, args.db, args.lang, args.strict) } diff --git a/website/data/shell/sorty.groovy b/website/data/shell/sorty.groovy index d7b12269..8c4602a8 100644 --- a/website/data/shell/sorty.groovy +++ b/website/data/shell/sorty.groovy @@ -20,7 +20,12 @@ def incomplete(f) { f =~ /[.]chunk|[.]part$/ } // all volumes complete, call unrar on first volume if (incomplete.isEmpty()) { - execute("unrar", "x", "-y", "-p-", rarP1.getAbsolutePath(), rarP1.getPathWithoutExtension() + "/") + def exitCode = execute("unrar", "x", "-y", "-p-", rarP1.getAbsolutePath(), rarP1.getPathWithoutExtension() + "/") + + // delete all volumes after successful extraction + if (exitCode == 0) { + volumes*.delete() + } } } diff --git a/website/data/shell/watcher.groovy b/website/data/shell/watcher.groovy new file mode 100644 index 00000000..3548f741 --- /dev/null +++ b/website/data/shell/watcher.groovy @@ -0,0 +1,8 @@ +def dirs = args.getFolders() + +// watch folders and print files that were added/modified (requires Java 7) +dirs.watch { println "Batch: " + it } // default commit delay is 5 minutes +dirs.watch { println "Quick: " + it }.setCommitDelay(100) // 100 ms commit delay + +println "Waiting for events" +console.readLine()