From 6aea967566cd07318ed6065797cc3ae22058f541 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Thu, 22 Dec 2011 19:36:31 +0000 Subject: [PATCH] * lots of work done on adding functionality to the scripting interface --- source/net/sourceforge/filebot/Settings.java | 53 +++---- .../filebot/cli/ArgumentProcessor.java | 7 +- .../sourceforge/filebot/cli/ScriptShell.java | 29 ++-- .../filebot/cli/ScriptShell.lib.groovy | 48 +++++-- .../format/AssociativeScriptObject.java | 71 ++++------ .../format/ExpressionFormat.lib.groovy | 3 +- .../filebot/format/PropertyBindings.java | 134 ++++++++++++++++++ .../filebot/mediainfo/ReleaseInfo.java | 2 +- .../filebot/similarity/DateMetric.java | 20 +-- .../filebot/similarity/EpisodeMetrics.java | 2 +- .../filebot/web/TheTVDBClient.java | 36 ++++- website/data/shell/banners.groovy | 108 ++++++++------ website/data/shell/housekeeping.groovy | 9 +- website/data/shell/rsam.groovy | 6 +- website/data/shell/sorty.groovy | 29 ++-- website/data/shell/watcher.groovy | 10 +- 16 files changed, 394 insertions(+), 173 deletions(-) create mode 100644 source/net/sourceforge/filebot/format/PropertyBindings.java diff --git a/source/net/sourceforge/filebot/Settings.java b/source/net/sourceforge/filebot/Settings.java index df1f1416..3bba7e37 100644 --- a/source/net/sourceforge/filebot/Settings.java +++ b/source/net/sourceforge/filebot/Settings.java @@ -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 entry(String key) { return new PreferencesEntry(prefs, key, new StringAdapter()); } - + public PreferencesMap asMap() { return PreferencesMap.map(prefs); } - + public PreferencesList 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"); diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 7b7d3399..c95fb16c 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -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); } diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.java b/source/net/sourceforge/filebot/cli/ScriptShell.java index cc18902b..9e16b258 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.java +++ b/source/net/sourceforge/filebot/cli/ScriptShell.java @@ -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("<>", "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.*")); diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index 7bb4ce19..3178c182 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -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 }} diff --git a/source/net/sourceforge/filebot/format/AssociativeScriptObject.java b/source/net/sourceforge/filebot/format/AssociativeScriptObject.java index f5012c35..9534c7e6 100644 --- a/source/net/sourceforge/filebot/format/AssociativeScriptObject.java +++ b/source/net/sourceforge/filebot/format/AssociativeScriptObject.java @@ -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 properties; + private final Map properties; - - public AssociativeScriptObject(Map 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(properties.keySet()).toString(); + return new TreeSet(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 { + private static class LenientLookup extends AbstractMap { - private final Map> lookup = new HashMap>(); + private final Map> lookup = new HashMap>(); - - public LenientLookup(Map source) { + + public LenientLookup(Map source) { // populate lookup map - for (Entry 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 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> entrySet() { - return new AbstractSet>() { + public Set> entrySet() { + return new AbstractSet>() { + @SuppressWarnings("unchecked") @Override - public Iterator> iterator() { - @SuppressWarnings("unchecked") - Iterator> iterator = (Iterator) lookup.values().iterator(); - return iterator; + public Iterator> iterator() { + return (Iterator) lookup.values().iterator(); } - + @Override public int size() { return lookup.size(); diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy b/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy index 4bca355c..b91022a2 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy +++ b/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy @@ -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) } /** diff --git a/source/net/sourceforge/filebot/format/PropertyBindings.java b/source/net/sourceforge/filebot/format/PropertyBindings.java new file mode 100644 index 00000000..4ce2d122 --- /dev/null +++ b/source/net/sourceforge/filebot/format/PropertyBindings.java @@ -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 { + + private final Object object; + private final Map properties = new TreeMap(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 keySet() { + return properties.keySet(); + } + + + @Override + public boolean isEmpty() { + return properties.isEmpty(); + } + + + @Override + public String toString() { + return properties.toString(); + } + + + @Override + public Set> entrySet() { + Set> entrySet = new HashSet>(); + + for (final String key : keySet()) { + entrySet.add(new Entry() { + + @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; + } + +} diff --git a/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java b/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java index 8b36e4ac..c5a73c5f 100644 --- a/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java +++ b/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java @@ -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())); diff --git a/source/net/sourceforge/filebot/similarity/DateMetric.java b/source/net/sourceforge/filebot/similarity/DateMetric.java index 9d20feff..f58ac212 100644 --- a/source/net/sourceforge/filebot/similarity/DateMetric.java +++ b/source/net/sourceforge/filebot/similarity/DateMetric.java @@ -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("(? - 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(''' + + $name + $firstAired.year + $rating + $ratingCount + $overview + $runtime + $contentRating + ${genre.size() > 0 ? genre.get(0) : ''} + $id + $bannerUrl + $firstAired.year + $status + $network + actors.each { + + $it + + } + + ''').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) diff --git a/website/data/shell/housekeeping.groovy b/website/data/shell/housekeeping.groovy index 69488a3b..2acef128 100644 --- a/website/data/shell/housekeeping.groovy +++ b/website/data/shell/housekeeping.groovy @@ -1,5 +1,9 @@ // filebot -script "http://filebot.sourceforge.net/data/shell/housekeeping.groovy" +// 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 { diff --git a/website/data/shell/rsam.groovy b/website/data/shell/rsam.groovy index 57f4c617..db709bef 100644 --- a/website/data/shell/rsam.groovy +++ b/website/data/shell/rsam.groovy @@ -1,8 +1,10 @@ // filebot -script "http://filebot.sourceforge.net/data/shell/rsam.groovy" -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) diff --git a/website/data/shell/sorty.groovy b/website/data/shell/sorty.groovy index 068ec159..8c894d6f 100644 --- a/website/data/shell/sorty.groovy +++ b/website/data/shell/sorty.groovy @@ -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) diff --git a/website/data/shell/watcher.groovy b/website/data/shell/watcher.groovy index 17e1312d..b719010f 100644 --- a/website/data/shell/watcher.groovy +++ b/website/data/shell/watcher.groovy @@ -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