* lots of work done on adding functionality to the scripting interface
This commit is contained in:
parent
0eff37b056
commit
6aea967566
|
@ -6,7 +6,6 @@ import static net.sourceforge.tuned.StringUtilities.*;
|
|||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.jar.Manifest;
|
||||
|
@ -28,17 +27,17 @@ public final class Settings {
|
|||
return getApplicationProperty("application.name");
|
||||
};
|
||||
|
||||
|
||||
|
||||
public static String getApplicationVersion() {
|
||||
return getApplicationProperty("application.version");
|
||||
};
|
||||
|
||||
|
||||
|
||||
public static String getApplicationProperty(String key) {
|
||||
return ResourceBundle.getBundle(Settings.class.getName(), Locale.ROOT).getString(key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getApplicationDeployment() {
|
||||
String deployment = System.getProperty("application.deployment");
|
||||
if (deployment != null)
|
||||
|
@ -50,7 +49,7 @@ public final class Settings {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static File getApplicationFolder() {
|
||||
// special handling for web start
|
||||
if (getApplicationDeployment() != null) {
|
||||
|
@ -69,60 +68,60 @@ public final class Settings {
|
|||
return new File(System.getProperty("user.dir"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static Settings forPackage(Class<?> type) {
|
||||
return new Settings(Preferences.userNodeForPackage(type));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private final Preferences prefs;
|
||||
|
||||
|
||||
|
||||
private Settings(Preferences prefs) {
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Settings node(String nodeName) {
|
||||
return new Settings(prefs.node(nodeName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String get(String key) {
|
||||
return get(key, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String get(String key, String def) {
|
||||
return prefs.get(key, def);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void put(String key, String value) {
|
||||
prefs.put(key, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void remove(String key) {
|
||||
prefs.remove(key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public PreferencesEntry<String> entry(String key) {
|
||||
return new PreferencesEntry<String>(prefs, key, new StringAdapter());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public PreferencesMap<String> asMap() {
|
||||
return PreferencesMap.map(prefs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public PreferencesList<String> asList() {
|
||||
return PreferencesList.map(prefs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void clear() {
|
||||
try {
|
||||
// remove child nodes
|
||||
|
@ -137,20 +136,24 @@ public final class Settings {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static String getApplicationIdentifier() {
|
||||
String rev = null;
|
||||
|
||||
public static int getApplicationRevisionNumber() {
|
||||
try {
|
||||
Manifest manifest = new Manifest(Settings.class.getResourceAsStream("/META-INF/MANIFEST.MF"));
|
||||
rev = manifest.getMainAttributes().getValue("Built-Revision");
|
||||
} catch (IOException e) {
|
||||
String rev = manifest.getMainAttributes().getValue("Built-Revision");
|
||||
return Integer.parseInt(rev);
|
||||
} catch (Exception e) {
|
||||
Logger.getLogger(Settings.class.getName()).log(Level.WARNING, e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
|
||||
return joinBy(" ", getApplicationName(), getApplicationVersion(), String.format("(r%s)", rev != null ? rev : 0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getApplicationIdentifier() {
|
||||
return joinBy(" ", getApplicationName(), getApplicationVersion(), String.format("(r%s)", getApplicationRevisionNumber()));
|
||||
}
|
||||
|
||||
|
||||
public static String getJavaRuntimeIdentifier() {
|
||||
String name = System.getProperty("java.runtime.name");
|
||||
String version = System.getProperty("java.version");
|
||||
|
|
|
@ -3,6 +3,7 @@ package net.sourceforge.filebot.cli;
|
|||
|
||||
|
||||
import static net.sourceforge.filebot.cli.CLILogging.*;
|
||||
import static net.sourceforge.tuned.ExceptionUtilities.*;
|
||||
import static net.sourceforge.tuned.FileUtilities.*;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -34,7 +35,7 @@ public class ArgumentProcessor {
|
|||
return bean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int process(ArgumentBean args, CmdlineInterface cli) throws Exception {
|
||||
Analytics.trackView(ArgumentProcessor.class, "FileBot CLI");
|
||||
CLILogger.setLevel(args.getLogLevel());
|
||||
|
@ -96,13 +97,13 @@ public class ArgumentProcessor {
|
|||
CLILogger.finest("Done ヾ(@⌒ー⌒@)ノ");
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
CLILogger.severe(String.format("%s: %s", e.getClass().getSimpleName(), e.getMessage()));
|
||||
CLILogger.severe(String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)));
|
||||
CLILogger.finest("Failure (°_°)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void printHelp(ArgumentBean argumentBean) {
|
||||
new CmdLineParser(argumentBean).printUsage(System.out);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.codehaus.groovy.jsr223.GroovyScriptEngineFactory;
|
|||
|
||||
import net.sourceforge.filebot.MediaTypes;
|
||||
import net.sourceforge.filebot.WebServices;
|
||||
import net.sourceforge.filebot.format.AssociativeScriptObject;
|
||||
import net.sourceforge.filebot.format.ExpressionFormat;
|
||||
import net.sourceforge.filebot.format.PrivilegedInvocation;
|
||||
import net.sourceforge.filebot.web.EpisodeListProvider;
|
||||
|
@ -55,21 +56,32 @@ class ScriptShell {
|
|||
|
||||
protected Bindings initializeBindings(CmdlineInterface cli, ArgumentBean args, AccessControlContext acc) {
|
||||
Bindings bindings = new SimpleBindings();
|
||||
bindings.put("_script", new File(args.script));
|
||||
|
||||
// bind API objects
|
||||
bindings.put("_cli", PrivilegedInvocation.newProxy(CmdlineInterface.class, cli, acc));
|
||||
bindings.put("_script", new File(args.script));
|
||||
bindings.put("_args", args);
|
||||
|
||||
bindings.put("_types", MediaTypes.getDefault());
|
||||
bindings.put("_log", CLILogger);
|
||||
|
||||
// initialize web services
|
||||
// bind Java properties and environment variables
|
||||
bindings.put("_prop", new AssociativeScriptObject(System.getProperties()));
|
||||
bindings.put("_env", new AssociativeScriptObject(System.getenv()));
|
||||
|
||||
// bind console object
|
||||
bindings.put("console", System.console());
|
||||
|
||||
// bind Episode data providers
|
||||
for (EpisodeListProvider service : WebServices.getEpisodeListProviders()) {
|
||||
bindings.put(service.getName().toLowerCase(), PrivilegedInvocation.newProxy(EpisodeListProvider.class, service, acc));
|
||||
}
|
||||
for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) {
|
||||
bindings.put(service.getName().toLowerCase(), PrivilegedInvocation.newProxy(MovieIdentificationService.class, service, acc));
|
||||
bindings.put(service.getName(), service);
|
||||
}
|
||||
|
||||
// bind Movie data providers
|
||||
for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) {
|
||||
bindings.put(service.getName(), service);
|
||||
}
|
||||
|
||||
bindings.put("console", System.console());
|
||||
return bindings;
|
||||
}
|
||||
|
||||
|
@ -97,8 +109,9 @@ class ScriptShell {
|
|||
Permissions permissions = new Permissions();
|
||||
|
||||
permissions.add(new RuntimePermission("createClassLoader"));
|
||||
permissions.add(new RuntimePermission("accessDeclaredMembers"));
|
||||
permissions.add(new RuntimePermission("accessDeclaredMembers")); // this is probably a security problem but nevermind
|
||||
permissions.add(new FilePermission("<<ALL FILES>>", "read"));
|
||||
permissions.add(new FilePermission(new File(System.getProperty("java.io.tmpdir")).getAbsolutePath() + File.separator, "write"));
|
||||
permissions.add(new SocketPermission("*", "connect"));
|
||||
permissions.add(new PropertyPermission("*", "read"));
|
||||
permissions.add(new RuntimePermission("getenv.*"));
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// File selector methods
|
||||
import static groovy.io.FileType.*
|
||||
|
||||
File.metaClass.node = { path -> new File(delegate, path) }
|
||||
File.metaClass.resolve = { Object name -> new File(delegate, name.toString()) }
|
||||
File.metaClass.getAt = { String name -> new File(delegate, name) }
|
||||
File.metaClass.listFiles = { c -> delegate.isDirectory() ? delegate.listFiles().findAll(c) : []}
|
||||
|
||||
File.metaClass.isVideo = { _types.getFilter("video").accept(delegate) }
|
||||
|
@ -27,7 +28,7 @@ List.metaClass.eachMediaFolder = { c -> getFolders{ it.hasFile{ it.isVideo() } }
|
|||
|
||||
|
||||
// File utility methods
|
||||
import static net.sourceforge.tuned.FileUtilities.*;
|
||||
import static net.sourceforge.tuned.FileUtilities.*
|
||||
|
||||
File.metaClass.getNameWithoutExtension = { getNameWithoutExtension(delegate.getName()) }
|
||||
File.metaClass.getPathWithoutExtension = { new File(delegate.getParentFile(), getNameWithoutExtension(delegate.getName())).getPath() }
|
||||
|
@ -41,13 +42,34 @@ List.metaClass.mapByFolder = { mapByFolder(delegate) }
|
|||
List.metaClass.mapByExtension = { mapByExtension(delegate) }
|
||||
String.metaClass.getExtension = { getExtension(delegate) }
|
||||
String.metaClass.hasExtension = { String... ext -> hasExtension(delegate, ext) }
|
||||
String.metaClass.validateFileName = { validateFileName(delegate) }
|
||||
String.metaClass.validateFilePath = { validateFilePath(delegate) }
|
||||
|
||||
|
||||
// WebRequest utility methods
|
||||
// Parallel helper
|
||||
import java.util.concurrent.*
|
||||
|
||||
def parallel(List closures, int threads = Runtime.getRuntime().availableProcessors()) {
|
||||
def tasks = closures.collect { it as Callable }
|
||||
return Executors.newFixedThreadPool(threads).invokeAll(tasks).collect{ it.get() }
|
||||
}
|
||||
|
||||
|
||||
// Web and File IO helpers
|
||||
import java.nio.charset.Charset;
|
||||
import static net.sourceforge.filebot.web.WebRequest.*
|
||||
|
||||
URL.metaClass.parseHtml = { new XmlParser(false, false).parseText(getXmlString(getHtmlDocument(delegate))) };
|
||||
URL.metaClass.parseHtml = { new XmlParser(false, false).parseText(getXmlString(getHtmlDocument(delegate))) }
|
||||
URL.metaClass.saveAs = { f -> writeFile(fetch(delegate), f); f.absolutePath }
|
||||
String.metaClass.saveAs = { f, csn = "utf-8" -> writeFile(Charset.forName(csn).encode(delegate), f); f.absolutePath }
|
||||
|
||||
// Template Engine helpers
|
||||
import groovy.text.XmlTemplateEngine
|
||||
import groovy.text.GStringTemplateEngine
|
||||
import net.sourceforge.filebot.format.PropertyBindings
|
||||
|
||||
Object.metaClass.applyXmlTemplate = { template -> new XmlTemplateEngine("\t", false).createTemplate(template).make(new PropertyBindings(delegate, "")).toString() }
|
||||
Object.metaClass.applyTextTemplate = { template -> new GStringTemplateEngine().createTemplate(template).make(new PropertyBindings(delegate, "")).toString() }
|
||||
|
||||
|
||||
// Shell helper
|
||||
|
@ -104,20 +126,28 @@ List.metaClass.watch = { c -> createWatchService(c, delegate, true) }
|
|||
|
||||
|
||||
// Season / Episode helpers
|
||||
import net.sourceforge.filebot.mediainfo.ReleaseInfo
|
||||
import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher
|
||||
import net.sourceforge.filebot.mediainfo.*
|
||||
import net.sourceforge.filebot.similarity.*
|
||||
|
||||
def guessEpisodeNumber(path) {
|
||||
def input = path instanceof File ? path.getName() : path.toString()
|
||||
def parseEpisodeNumber(path) {
|
||||
def input = path instanceof File ? path.name : path.toString()
|
||||
def sxe = new SeasonEpisodeMatcher(new SeasonEpisodeMatcher.SeasonEpisodeFilter(30, 50, 1000)).match(input)
|
||||
return sxe == null || sxe.isEmpty() ? null : sxe[0]
|
||||
}
|
||||
|
||||
def parseDate(path) {
|
||||
return new DateMetric().parse(input)
|
||||
}
|
||||
|
||||
def detectSeriesName(files) {
|
||||
def names = ReleaseInfo.detectSeriesNames(files.findAll { it.isVideo() || it.isSubtitle() })
|
||||
return names == null || names.isEmpty() ? null : names[0]
|
||||
}
|
||||
|
||||
def similarity(o1, o2) {
|
||||
return new NameSimilarityMetric().getSimilarity(o1, o2)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// CLI bindings
|
||||
|
@ -194,4 +224,4 @@ def _defaults(args) {
|
|||
/**
|
||||
* Catch and log exceptions thrown by the closure
|
||||
*/
|
||||
this.metaClass._guarded = { c -> try { return c.call() } catch (e) { _log.severe(e.getMessage()); return null }}
|
||||
this.metaClass._guarded = { c -> try { return c.call() } catch (e) { _log.severe("${e.class.simpleName}: ${e.message}"); return null }}
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
package net.sourceforge.filebot.format;
|
||||
|
||||
|
||||
import groovy.lang.GroovyObject;
|
||||
import groovy.lang.MetaClass;
|
||||
import groovy.lang.GroovyObjectSupport;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.AbstractSet;
|
||||
|
@ -14,16 +13,16 @@ import java.util.Set;
|
|||
import java.util.TreeSet;
|
||||
|
||||
|
||||
public class AssociativeScriptObject implements GroovyObject {
|
||||
public class AssociativeScriptObject extends GroovyObjectSupport {
|
||||
|
||||
private final Map<String, Object> properties;
|
||||
private final Map<?, ?> properties;
|
||||
|
||||
|
||||
public AssociativeScriptObject(Map<String, ?> properties) {
|
||||
|
||||
public AssociativeScriptObject(Map<?, ?> properties) {
|
||||
this.properties = new LenientLookup(properties);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the property with the given name.
|
||||
*
|
||||
|
@ -40,71 +39,52 @@ public class AssociativeScriptObject implements GroovyObject {
|
|||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void setProperty(String name, Object value) {
|
||||
// ignore, object is immutable
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object invokeMethod(String name, Object args) {
|
||||
// ignore, object is merely a structure
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public MetaClass getMetaClass() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setMetaClass(MetaClass clazz) {
|
||||
// ignore, don't care about MetaClass
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// all the properties in alphabetic order
|
||||
return new TreeSet<String>(properties.keySet()).toString();
|
||||
return new TreeSet<Object>(properties.keySet()).toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Map allowing look-up of values by a fault-tolerant key as specified by the defining key.
|
||||
*
|
||||
*/
|
||||
private static class LenientLookup extends AbstractMap<String, Object> {
|
||||
private static class LenientLookup extends AbstractMap<Object, Object> {
|
||||
|
||||
private final Map<String, Entry<String, ?>> lookup = new HashMap<String, Entry<String, ?>>();
|
||||
private final Map<String, Entry<?, ?>> lookup = new HashMap<String, Entry<?, ?>>();
|
||||
|
||||
|
||||
public LenientLookup(Map<String, ?> source) {
|
||||
|
||||
public LenientLookup(Map<?, ?> source) {
|
||||
// populate lookup map
|
||||
for (Entry<String, ?> entry : source.entrySet()) {
|
||||
for (Entry<?, ?> entry : source.entrySet()) {
|
||||
lookup.put(definingKey(entry.getKey()), entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected String definingKey(Object key) {
|
||||
// letters and digits are defining, everything else will be ignored
|
||||
return key.toString().replaceAll("[^\\p{Alnum}]", "").toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return lookup.containsKey(definingKey(key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
Entry<String, ?> entry = lookup.get(definingKey(key));
|
||||
Entry<?, ?> entry = lookup.get(definingKey(key));
|
||||
|
||||
if (entry != null)
|
||||
return entry.getValue();
|
||||
|
@ -112,19 +92,18 @@ public class AssociativeScriptObject implements GroovyObject {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
return new AbstractSet<Entry<String, Object>>() {
|
||||
public Set<Entry<Object, Object>> entrySet() {
|
||||
return new AbstractSet<Entry<Object, Object>>() {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Iterator<Entry<String, Object>> iterator() {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<Entry<String, Object>> iterator = (Iterator) lookup.values().iterator();
|
||||
return iterator;
|
||||
public Iterator<Entry<Object, Object>> iterator() {
|
||||
return (Iterator) lookup.values().iterator();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return lookup.size();
|
||||
|
|
|
@ -7,7 +7,8 @@ import static net.sourceforge.tuned.FileUtilities.*;
|
|||
*
|
||||
* e.g. file[0] -> "F:"
|
||||
*/
|
||||
File.metaClass.getAt = { index -> listPath(delegate).collect{ replacePathSeparators(getName(it)).trim() }.getAt(index) }
|
||||
File.metaClass.getAt = { Range range -> listPath(delegate).collect{ replacePathSeparators(getName(it)).trim() }.getAt(range).join(File.separator) }
|
||||
File.metaClass.getAt = { int index -> listPath(delegate).collect{ replacePathSeparators(getName(it)).trim() }.getAt(index) }
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
|
||||
package net.sourceforge.filebot.format;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
|
||||
/*
|
||||
* Used to create a map view of the properties of an Object
|
||||
*/
|
||||
public class PropertyBindings extends AbstractMap<String, Object> {
|
||||
|
||||
private final Object object;
|
||||
private final Map<String, Object> properties = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private final Object defaultValue;
|
||||
|
||||
|
||||
public PropertyBindings(Object object, Object defaultValue) {
|
||||
this.object = object;
|
||||
this.defaultValue = defaultValue;
|
||||
|
||||
// get method bindings
|
||||
for (Method method : object.getClass().getMethods()) {
|
||||
if (method.getReturnType() != void.class && method.getParameterTypes().length == 0) {
|
||||
// normal properties
|
||||
if (method.getName().length() > 3 && method.getName().substring(0, 3).equalsIgnoreCase("get")) {
|
||||
properties.put(method.getName().substring(3), method);
|
||||
}
|
||||
|
||||
// boolean properties
|
||||
if (method.getName().length() > 2 && method.getName().substring(0, 3).equalsIgnoreCase("is")) {
|
||||
properties.put(method.getName().substring(2), method);
|
||||
}
|
||||
|
||||
// just bind all methods to their original name as well
|
||||
properties.put(method.getName(), method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
Object value = properties.get(key);
|
||||
|
||||
// evaluate method
|
||||
if (value instanceof Method) {
|
||||
try {
|
||||
value = ((Method) value).invoke(object);
|
||||
|
||||
if (value == null) {
|
||||
value = defaultValue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
return properties.put(key, value);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object remove(Object key) {
|
||||
return properties.remove(key);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return properties.containsKey(key);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return properties.keySet();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return properties.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return properties.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
Set<Entry<String, Object>> entrySet = new HashSet<Entry<String, Object>>();
|
||||
|
||||
for (final String key : keySet()) {
|
||||
entrySet.add(new Entry<String, Object>() {
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return get(key);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object setValue(Object value) {
|
||||
return put(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return entrySet;
|
||||
}
|
||||
|
||||
}
|
|
@ -128,7 +128,7 @@ public class ReleaseInfo {
|
|||
for (String token : Pattern.compile("[\\s\"<>|]+").split(text)) {
|
||||
try {
|
||||
URL url = new URL(token);
|
||||
if (url.getHost().contains("thetvdb")) {
|
||||
if (url.getHost().contains("thetvdb") && url.getQuery() != null) {
|
||||
Matcher idMatch = Pattern.compile("(?<=(^|\\W)id=)\\d+").matcher(url.getQuery());
|
||||
while (idMatch.find()) {
|
||||
collection.add(Integer.parseInt(idMatch.group()));
|
||||
|
|
|
@ -14,7 +14,7 @@ public class DateMetric implements SimilarityMetric {
|
|||
|
||||
private final DatePattern[] patterns;
|
||||
|
||||
|
||||
|
||||
public DateMetric() {
|
||||
patterns = new DatePattern[2];
|
||||
|
||||
|
@ -25,7 +25,7 @@ public class DateMetric implements SimilarityMetric {
|
|||
patterns[1] = new DatePattern("(?<!\\p{Alnum})(\\d{1,2})[^\\p{Alnum}](\\d{1,2})[^\\p{Alnum}](\\d{4})(?!\\p{Alnum})", new int[] { 3, 2, 1 });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
Date d1 = parse(o1);
|
||||
|
@ -39,8 +39,8 @@ public class DateMetric implements SimilarityMetric {
|
|||
return d1.equals(d2) ? 1 : -1;
|
||||
}
|
||||
|
||||
|
||||
protected Date parse(Object object) {
|
||||
|
||||
public Date parse(Object object) {
|
||||
if (object instanceof File) {
|
||||
// parse file name
|
||||
object = ((File) object).getName();
|
||||
|
@ -49,8 +49,8 @@ public class DateMetric implements SimilarityMetric {
|
|||
return match(object.toString());
|
||||
}
|
||||
|
||||
|
||||
protected Date match(CharSequence name) {
|
||||
|
||||
public Date match(CharSequence name) {
|
||||
for (DatePattern pattern : patterns) {
|
||||
Date match = pattern.match(name);
|
||||
|
||||
|
@ -62,24 +62,24 @@ public class DateMetric implements SimilarityMetric {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected static class DatePattern {
|
||||
|
||||
protected final Pattern pattern;
|
||||
protected final int[] order;
|
||||
|
||||
|
||||
|
||||
public DatePattern(String pattern, int[] order) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected Date process(MatchResult match) {
|
||||
return new Date(Integer.parseInt(match.group(order[0])), Integer.parseInt(match.group(order[1])), Integer.parseInt(match.group(order[2])));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Date match(CharSequence name) {
|
||||
Matcher matcher = pattern.matcher(name);
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ public enum EpisodeMetrics implements SimilarityMetric {
|
|||
|
||||
|
||||
@Override
|
||||
protected Date parse(Object object) {
|
||||
public Date parse(Object object) {
|
||||
if (object instanceof Movie) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -433,7 +433,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider {
|
|||
BannerMirror,
|
||||
banner,
|
||||
fanart,
|
||||
poster,
|
||||
poster
|
||||
}
|
||||
|
||||
|
||||
|
@ -556,29 +556,51 @@ public class TheTVDBClient extends AbstractEpisodeListProvider {
|
|||
}
|
||||
|
||||
|
||||
public String getNetwork() {
|
||||
// e.g. CBS
|
||||
return get(SeriesProperty.Network);
|
||||
}
|
||||
|
||||
|
||||
public String getStatus() {
|
||||
// e.g. Continuing
|
||||
return get(SeriesProperty.Status);
|
||||
}
|
||||
|
||||
|
||||
public URL getBannerMirrorUrl() throws MalformedURLException {
|
||||
return new URL(get(BannerProperty.BannerMirror));
|
||||
public URL getBannerMirrorUrl() {
|
||||
try {
|
||||
return new URL(get(BannerProperty.BannerMirror));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public URL getBannerUrl() throws MalformedURLException {
|
||||
return new URL(getBannerMirrorUrl(), get(SeriesProperty.banner));
|
||||
try {
|
||||
return new URL(getBannerMirrorUrl(), get(SeriesProperty.banner));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public URL getFanartUrl() throws MalformedURLException {
|
||||
return new URL(getBannerMirrorUrl(), get(SeriesProperty.fanart));
|
||||
public URL getFanartUrl() {
|
||||
try {
|
||||
return new URL(getBannerMirrorUrl(), get(SeriesProperty.fanart));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public URL getPosterUrl() throws MalformedURLException {
|
||||
return new URL(getBannerMirrorUrl(), get(SeriesProperty.poster));
|
||||
try {
|
||||
return new URL(getBannerMirrorUrl(), get(SeriesProperty.poster));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,77 +1,101 @@
|
|||
// filebot -script "http://filebot.sourceforge.net/data/shell/banners.groovy" -trust-script /path/to/media/
|
||||
|
||||
// EXPERIMENTAL // HERE THERE BE DRAGONS
|
||||
if (net.sourceforge.filebot.Settings.applicationRevisionNumber < 783) throw new Exception("Application revision too old")
|
||||
|
||||
|
||||
/*
|
||||
* Fetch series and season banners for all tv shows
|
||||
*/
|
||||
import static net.sourceforge.filebot.WebServices.*
|
||||
|
||||
|
||||
def fetchBanner(outputDir, outputName, series, bannerType, bannerType2, season = null) {
|
||||
def fetchBanner(outputFile, series, bannerType, bannerType2, season = null) {
|
||||
// select and fetch banner
|
||||
def banner = TheTVDB.getBanner(series, bannerType, bannerType2, season, Locale.ENGLISH, 0)
|
||||
if (banner == null) {
|
||||
println "Banner not found: $outputName"
|
||||
println "Banner not found: $outputFile"
|
||||
return null
|
||||
}
|
||||
|
||||
println "Fetching banner $banner"
|
||||
return banner.url.saveAs(new File(outputDir, outputName + ".jpg"))
|
||||
return banner.url.saveAs(outputFile)
|
||||
}
|
||||
|
||||
|
||||
def fetchNfo(outputDir, outputName, series) {
|
||||
def info = TheTVDB.getSeriesInfo(series, Locale.ENGLISH)
|
||||
println "Writing nfo $info"
|
||||
|
||||
new File(outputDir, outputName + ".nfo").withWriter{ out ->
|
||||
out.println("Name: $info.name")
|
||||
out.println("IMDb: http://www.imdb.com/title/tt${info.imdbId.pad(7)}")
|
||||
out.println("Actors: ${info.actors.join(', ')}")
|
||||
out.println("Genere: ${info.genre.join(', ')}")
|
||||
out.println("Language: ${info.language.displayName}")
|
||||
out.println("Overview: $info.overview")
|
||||
}
|
||||
def fetchNfo(outputFile, series) {
|
||||
TheTVDB.getSeriesInfo(series, Locale.ENGLISH).applyXmlTemplate('''
|
||||
<tvshow xmlns:gsp='http://groovy.codehaus.org/2005/gsp'>
|
||||
<title>$name</title>
|
||||
<year>$firstAired.year</year>
|
||||
<rating>$rating</rating>
|
||||
<votes>$ratingCount</votes>
|
||||
<plot>$overview</plot>
|
||||
<runtime>$runtime</runtime>
|
||||
<mpaa>$contentRating</mpaa>
|
||||
<genre>${genre.size() > 0 ? genre.get(0) : ''}</genre>
|
||||
<id>$id</id>
|
||||
<thumb>$bannerUrl</thumb>
|
||||
<premiered>$firstAired.year</premiered>
|
||||
<status>$status</status>
|
||||
<studio>$network</studio>
|
||||
<gsp:scriptlet> actors.each { </gsp:scriptlet>
|
||||
<actor>
|
||||
<name>$it</name>
|
||||
</actor>
|
||||
<gsp:scriptlet> } </gsp:scriptlet>
|
||||
</tvshow>
|
||||
''').saveAs(outputFile)
|
||||
}
|
||||
|
||||
|
||||
def fetchSeriesBannersAndNfo(dir, series, seasons) {
|
||||
println "Fetch nfo and banners for $series / Season $seasons"
|
||||
|
||||
def fetchSeriesBannersAndNfo(seriesDir, seasonDir, series, season) {
|
||||
println "Fetch nfo and banners for $series / Season $season"
|
||||
|
||||
// fetch nfo
|
||||
fetchNfo(dir, series.name, series)
|
||||
|
||||
fetchNfo(seriesDir['tvshow.nfo'], series)
|
||||
|
||||
// fetch series banner, fanart, posters, etc
|
||||
fetchBanner(dir, "folder", series, "poster", "680x1000")
|
||||
fetchBanner(dir, "banner", series, "series", "graphical")
|
||||
fetchBanner(seriesDir['folder.jpg'], series, "poster", "680x1000")
|
||||
fetchBanner(seriesDir['banner.jpg'], series, "series", "graphical")
|
||||
|
||||
// fetch highest resolution fanart
|
||||
["1920x1080", "1280x720"].findResult{ bannerType2 -> fetchBanner(dir, "fanart", series, "fanart", bannerType2) }
|
||||
["1920x1080", "1280x720"].findResult{ fetchBanner(seriesDir["fanart.jpg"], series, "fanart", it) }
|
||||
|
||||
// fetch season banners
|
||||
seasons.each { s ->
|
||||
fetchBanner(dir, "folder-S${s.pad(2)}", series, "season", "season", s)
|
||||
fetchBanner(dir, "banner-S${s.pad(2)}", series, "season", "seasonwide", s)
|
||||
if (seasonDir != seriesDir) {
|
||||
fetchBanner(seasonDir["folder.jpg"], series, "season", "season", season)
|
||||
fetchBanner(seasonDir["banner.jpg"], series, "season", "seasonwide", season)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
args.eachMediaFolder() { dir ->
|
||||
println "Processing $dir"
|
||||
def videoFiles = dir.listFiles{ it.isVideo() }
|
||||
|
||||
def seriesName = detectSeriesName(videoFiles)
|
||||
def seasons = videoFiles.findResults { guessEpisodeNumber(it)?.season }.unique()
|
||||
|
||||
if (seriesName == null) {
|
||||
println "Failed to detect series name from files -> Query by ${dir.name} instead"
|
||||
seriesName = dir.name
|
||||
def jobs = args.getFolders().findResults { dir ->
|
||||
def videos = dir.listFiles{ it.isVideo() }
|
||||
if (videos.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
def options = TheTVDB.search(seriesName)
|
||||
def query = _args.query ?: detectSeriesName(videos)
|
||||
def sxe = videos.findResult{ parseEpisodeNumber(it) }
|
||||
|
||||
if (query == null) {
|
||||
query = dir.name
|
||||
println "Failed to detect series name from video files -> Query by $query instead"
|
||||
}
|
||||
|
||||
def options = TheTVDB.search(query, Locale.ENGLISH)
|
||||
if (options.isEmpty()) {
|
||||
println "TV Series not found: $seriesName"
|
||||
return;
|
||||
println "TV Series not found: $query"
|
||||
return null;
|
||||
}
|
||||
|
||||
fetchSeriesBannersAndNfo(dir, options[0], seasons)
|
||||
// auto-select series
|
||||
def series = options[0]
|
||||
|
||||
// auto-detect structure
|
||||
def seriesDir = similarity(dir.dir.name, series.name) > 0.8 ? dir.dir : dir
|
||||
def season = sxe && sxe.season > 0 ? sxe.season : 1
|
||||
|
||||
return { fetchSeriesBannersAndNfo(seriesDir, dir, series, season) }
|
||||
}
|
||||
|
||||
parallel(jobs, 10)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
// filebot -script "http://filebot.sourceforge.net/data/shell/housekeeping.groovy" <folder>
|
||||
|
||||
// EXPERIMENTAL // HERE THERE BE DRAGONS
|
||||
if (net.sourceforge.filebot.Settings.applicationRevisionNumber < 783) throw new Exception("Revision 783+ required")
|
||||
|
||||
|
||||
/*
|
||||
* Watch folder for new tv shows and automatically
|
||||
* move/rename new episodes into a predefined folder structure
|
||||
|
@ -8,11 +12,8 @@
|
|||
// check for new media files once every 5 seconds
|
||||
def updateFrequency = 5 * 1000;
|
||||
|
||||
// V:/path for windows /usr/home/name/ for unix
|
||||
def destinationRoot = "{com.sun.jna.Platform.isWindows() ? file.path[0..1] : System.getProperty('user.home')}"
|
||||
|
||||
// V:/TV Shows/Stargate/Season 1/Stargate.S01E01.Pilot
|
||||
def episodeFormat = destinationRoot + "/TV Shows/{n}{'/Season '+s}/{n.space('.')}.{s00e00}.{t.space('.')}"
|
||||
def episodeFormat = "{com.sun.jna.Platform.isWindows() ? file[0] : home}/TV Shows/{n}{'/Season '+s}/{n.space('.')}.{s00e00}.{t.space('.')}"
|
||||
|
||||
// spawn daemon thread
|
||||
Thread.startDaemon {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
// filebot -script "http://filebot.sourceforge.net/data/shell/rsam.groovy" <options> <folder>
|
||||
|
||||
import net.sourceforge.filebot.similarity.*
|
||||
// EXPERIMENTAL // HERE THERE BE DRAGONS
|
||||
if (net.sourceforge.filebot.Settings.applicationRevisionNumber < 783) throw new Exception("Revision 783+ required")
|
||||
|
||||
def isMatch(a, b) { new NameSimilarityMetric().getSimilarity(a, b) > 0.9 }
|
||||
|
||||
def isMatch(a, b) { similarity(a, b) > 0.9 }
|
||||
|
||||
/*
|
||||
* Rename anime, tv shows or movies (assuming each folder represents one item)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
// Settings
|
||||
def episodeDir = "X:/in/TV"
|
||||
def movieDir = "X:/in/Movies"
|
||||
// EXPERIMENTAL // HERE THERE BE DRAGONS
|
||||
|
||||
def episodeFormat = "X:/out/TV/{n}{'/Season '+s}/{episode}"
|
||||
def movieFormat = "X:/out/Movies/{movie}/{movie}"
|
||||
// PERSONALIZED SETTINGS
|
||||
def episodeDir = "V:/in/TV"
|
||||
def episodeFormat = "V:/out/TV/{n}{'/Season '+s}/{episode}"
|
||||
def movieDir = "V:/in/Movies"
|
||||
def movieFormat = "V:/out/Movies/{movie}/{movie}"
|
||||
|
||||
// ignore chunk, part, par and hidden files
|
||||
def incomplete(f) { f.name =~ /[.]chunk|[.]part$|[.]par$/ || f.isHidden() }
|
||||
|
||||
def incomplete(f) { f =~ /[.]chunk|[.]part$/ }
|
||||
|
||||
// run cmdline unrar (require -trust-script) on multi-volume rar files
|
||||
[episodeDir, movieDir].getFiles().findAll {
|
||||
it =~ /[.]part01[.]rar$/ || (it =~ /[.]rar$/ && !(it =~ /[.]part\d{2}[.]rar$/))
|
||||
}.each { rar ->
|
||||
[episodeDir, movieDir].getFiles().findAll { it =~ /[.]part01[.]rar$/ || (it =~ /[.]rar$/ && !(it =~ /[.]part\d{2}[.]rar$/)) }.each { rar ->
|
||||
// new layout: foo.part1.rar, foo.part2.rar
|
||||
// old layout: foo.rar, foo.r00, foo.r01
|
||||
boolean partLayout = (rar =~ /[.]part01[.]rar/)
|
||||
|
@ -41,8 +42,9 @@ def incomplete(f) { f =~ /[.]chunk|[.]part$/ }
|
|||
/*
|
||||
* Fetch subtitles and sort into folders
|
||||
*/
|
||||
episodeDir.eachMediaFolder() { dir ->
|
||||
def files = dir.listFiles { !incomplete(it) }
|
||||
episodeDir.getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it.isVideo() } }.each{ dir ->
|
||||
println "Processing $dir"
|
||||
def files = dir.listFiles{ it.isVideo() }
|
||||
|
||||
// fetch subtitles
|
||||
files += getSubtitles(file:files)
|
||||
|
@ -51,8 +53,9 @@ episodeDir.eachMediaFolder() { dir ->
|
|||
rename(file:files, db:'TVRage', format:episodeFormat)
|
||||
}
|
||||
|
||||
movieDir.eachMediaFolder() { dir ->
|
||||
def files = dir.listFiles { !incomplete(it) }
|
||||
movieDir.getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it.isVideo() } }.each{ dir ->
|
||||
println "Processing $dir"
|
||||
def files = dir.listFiles{ it.isVideo() }
|
||||
|
||||
// fetch subtitles
|
||||
files += getSubtitles(file:files)
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
// EXPERIMENTAL // HERE THERE BE DRAGONS
|
||||
|
||||
// BEGIN SANITY CHECK
|
||||
if (_prop['java.runtime.version'] < '1.7') throw new Exception('Java 7 required')
|
||||
if (!(new File(_args.format ?: '').absolute)) throw new Exception('Absolute target path format required')
|
||||
// END
|
||||
|
||||
|
||||
// watch folders and print files that were added/modified (requires Java 7)
|
||||
def watchman = args.watch { changes ->
|
||||
println "Processing $changes"
|
||||
rename(file:changes, format:"/media/storage/files/tv/{n}{'/Season '+s}/{episode}")
|
||||
rename(file:changes)
|
||||
}
|
||||
|
||||
// process after 10 minutes without any changes to the folder
|
||||
|
|
Loading…
Reference in New Issue