* lots of work done on adding functionality to the scripting interface

This commit is contained in:
Reinhard Pointner 2011-12-22 19:36:31 +00:00
parent 0eff37b056
commit 6aea967566
16 changed files with 394 additions and 173 deletions

View File

@ -6,7 +6,6 @@ import static net.sourceforge.tuned.StringUtilities.*;
import java.awt.GraphicsEnvironment; import java.awt.GraphicsEnvironment;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Locale; import java.util.Locale;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.jar.Manifest; import java.util.jar.Manifest;
@ -138,16 +137,20 @@ public final class Settings {
} }
public static String getApplicationIdentifier() { public static int getApplicationRevisionNumber() {
String rev = null;
try { try {
Manifest manifest = new Manifest(Settings.class.getResourceAsStream("/META-INF/MANIFEST.MF")); Manifest manifest = new Manifest(Settings.class.getResourceAsStream("/META-INF/MANIFEST.MF"));
rev = manifest.getMainAttributes().getValue("Built-Revision"); String rev = manifest.getMainAttributes().getValue("Built-Revision");
} catch (IOException e) { return Integer.parseInt(rev);
} catch (Exception e) {
Logger.getLogger(Settings.class.getName()).log(Level.WARNING, e.getMessage()); 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()));
} }

View File

@ -3,6 +3,7 @@ package net.sourceforge.filebot.cli;
import static net.sourceforge.filebot.cli.CLILogging.*; import static net.sourceforge.filebot.cli.CLILogging.*;
import static net.sourceforge.tuned.ExceptionUtilities.*;
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*;
import java.io.File; import java.io.File;
@ -96,7 +97,7 @@ public class ArgumentProcessor {
CLILogger.finest("Done ヾ(@⌒ー⌒@)"); CLILogger.finest("Done ヾ(@⌒ー⌒@)");
return 0; return 0;
} catch (Exception e) { } 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 (°_°)"); CLILogger.finest("Failure (°_°)");
return -1; return -1;
} }

View File

@ -27,6 +27,7 @@ import org.codehaus.groovy.jsr223.GroovyScriptEngineFactory;
import net.sourceforge.filebot.MediaTypes; import net.sourceforge.filebot.MediaTypes;
import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.WebServices;
import net.sourceforge.filebot.format.AssociativeScriptObject;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.format.PrivilegedInvocation; import net.sourceforge.filebot.format.PrivilegedInvocation;
import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.EpisodeListProvider;
@ -55,21 +56,32 @@ class ScriptShell {
protected Bindings initializeBindings(CmdlineInterface cli, ArgumentBean args, AccessControlContext acc) { protected Bindings initializeBindings(CmdlineInterface cli, ArgumentBean args, AccessControlContext acc) {
Bindings bindings = new SimpleBindings(); 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("_cli", PrivilegedInvocation.newProxy(CmdlineInterface.class, cli, acc));
bindings.put("_script", new File(args.script));
bindings.put("_args", args); bindings.put("_args", args);
bindings.put("_types", MediaTypes.getDefault()); bindings.put("_types", MediaTypes.getDefault());
bindings.put("_log", CLILogger); 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()) { for (EpisodeListProvider service : WebServices.getEpisodeListProviders()) {
bindings.put(service.getName().toLowerCase(), PrivilegedInvocation.newProxy(EpisodeListProvider.class, service, acc)); bindings.put(service.getName(), service);
} }
for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) {
bindings.put(service.getName().toLowerCase(), PrivilegedInvocation.newProxy(MovieIdentificationService.class, service, acc)); // bind Movie data providers
for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) {
bindings.put(service.getName(), service);
} }
bindings.put("console", System.console());
return bindings; return bindings;
} }
@ -97,8 +109,9 @@ class ScriptShell {
Permissions permissions = new Permissions(); Permissions permissions = new Permissions();
permissions.add(new RuntimePermission("createClassLoader")); 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("<<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 SocketPermission("*", "connect"));
permissions.add(new PropertyPermission("*", "read")); permissions.add(new PropertyPermission("*", "read"));
permissions.add(new RuntimePermission("getenv.*")); permissions.add(new RuntimePermission("getenv.*"));

View File

@ -1,7 +1,8 @@
// File selector methods // File selector methods
import static groovy.io.FileType.* 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.listFiles = { c -> delegate.isDirectory() ? delegate.listFiles().findAll(c) : []}
File.metaClass.isVideo = { _types.getFilter("video").accept(delegate) } File.metaClass.isVideo = { _types.getFilter("video").accept(delegate) }
@ -27,7 +28,7 @@ List.metaClass.eachMediaFolder = { c -> getFolders{ it.hasFile{ it.isVideo() } }
// File utility methods // File utility methods
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*
File.metaClass.getNameWithoutExtension = { getNameWithoutExtension(delegate.getName()) } File.metaClass.getNameWithoutExtension = { getNameWithoutExtension(delegate.getName()) }
File.metaClass.getPathWithoutExtension = { new File(delegate.getParentFile(), getNameWithoutExtension(delegate.getName())).getPath() } 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) } List.metaClass.mapByExtension = { mapByExtension(delegate) }
String.metaClass.getExtension = { getExtension(delegate) } String.metaClass.getExtension = { getExtension(delegate) }
String.metaClass.hasExtension = { String... ext -> hasExtension(delegate, ext) } 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.* 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 } 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 // Shell helper
@ -104,20 +126,28 @@ List.metaClass.watch = { c -> createWatchService(c, delegate, true) }
// Season / Episode helpers // Season / Episode helpers
import net.sourceforge.filebot.mediainfo.ReleaseInfo import net.sourceforge.filebot.mediainfo.*
import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher import net.sourceforge.filebot.similarity.*
def guessEpisodeNumber(path) { def parseEpisodeNumber(path) {
def input = path instanceof File ? path.getName() : path.toString() def input = path instanceof File ? path.name : path.toString()
def sxe = new SeasonEpisodeMatcher(new SeasonEpisodeMatcher.SeasonEpisodeFilter(30, 50, 1000)).match(input) def sxe = new SeasonEpisodeMatcher(new SeasonEpisodeMatcher.SeasonEpisodeFilter(30, 50, 1000)).match(input)
return sxe == null || sxe.isEmpty() ? null : sxe[0] return sxe == null || sxe.isEmpty() ? null : sxe[0]
} }
def parseDate(path) {
return new DateMetric().parse(input)
}
def detectSeriesName(files) { def detectSeriesName(files) {
def names = ReleaseInfo.detectSeriesNames(files.findAll { it.isVideo() || it.isSubtitle() }) def names = ReleaseInfo.detectSeriesNames(files.findAll { it.isVideo() || it.isSubtitle() })
return names == null || names.isEmpty() ? null : names[0] return names == null || names.isEmpty() ? null : names[0]
} }
def similarity(o1, o2) {
return new NameSimilarityMetric().getSimilarity(o1, o2)
}
// CLI bindings // CLI bindings
@ -194,4 +224,4 @@ def _defaults(args) {
/** /**
* Catch and log exceptions thrown by the closure * 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 }}

View File

@ -2,8 +2,7 @@
package net.sourceforge.filebot.format; package net.sourceforge.filebot.format;
import groovy.lang.GroovyObject; import groovy.lang.GroovyObjectSupport;
import groovy.lang.MetaClass;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.AbstractSet; import java.util.AbstractSet;
@ -14,12 +13,12 @@ import java.util.Set;
import java.util.TreeSet; 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); this.properties = new LenientLookup(properties);
} }
@ -47,29 +46,10 @@ public class AssociativeScriptObject implements GroovyObject {
} }
@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 @Override
public String toString() { public String toString() {
// all the properties in alphabetic order // all the properties in alphabetic order
return new TreeSet<String>(properties.keySet()).toString(); return new TreeSet<Object>(properties.keySet()).toString();
} }
@ -77,14 +57,14 @@ public class AssociativeScriptObject implements GroovyObject {
* Map allowing look-up of values by a fault-tolerant key as specified by the defining key. * 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 // populate lookup map
for (Entry<String, ?> entry : source.entrySet()) { for (Entry<?, ?> entry : source.entrySet()) {
lookup.put(definingKey(entry.getKey()), entry); lookup.put(definingKey(entry.getKey()), entry);
} }
} }
@ -104,7 +84,7 @@ public class AssociativeScriptObject implements GroovyObject {
@Override @Override
public Object get(Object key) { public Object get(Object key) {
Entry<String, ?> entry = lookup.get(definingKey(key)); Entry<?, ?> entry = lookup.get(definingKey(key));
if (entry != null) if (entry != null)
return entry.getValue(); return entry.getValue();
@ -114,14 +94,13 @@ public class AssociativeScriptObject implements GroovyObject {
@Override @Override
public Set<Entry<String, Object>> entrySet() { public Set<Entry<Object, Object>> entrySet() {
return new AbstractSet<Entry<String, Object>>() { return new AbstractSet<Entry<Object, Object>>() {
@SuppressWarnings("unchecked")
@Override @Override
public Iterator<Entry<String, Object>> iterator() { public Iterator<Entry<Object, Object>> iterator() {
@SuppressWarnings("unchecked") return (Iterator) lookup.values().iterator();
Iterator<Entry<String, Object>> iterator = (Iterator) lookup.values().iterator();
return iterator;
} }

View File

@ -7,7 +7,8 @@ import static net.sourceforge.tuned.FileUtilities.*;
* *
* e.g. file[0] -> "F:" * 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) }
/** /**

View File

@ -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;
}
}

View File

@ -128,7 +128,7 @@ public class ReleaseInfo {
for (String token : Pattern.compile("[\\s\"<>|]+").split(text)) { for (String token : Pattern.compile("[\\s\"<>|]+").split(text)) {
try { try {
URL url = new URL(token); 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()); Matcher idMatch = Pattern.compile("(?<=(^|\\W)id=)\\d+").matcher(url.getQuery());
while (idMatch.find()) { while (idMatch.find()) {
collection.add(Integer.parseInt(idMatch.group())); collection.add(Integer.parseInt(idMatch.group()));

View File

@ -40,7 +40,7 @@ public class DateMetric implements SimilarityMetric {
} }
protected Date parse(Object object) { public Date parse(Object object) {
if (object instanceof File) { if (object instanceof File) {
// parse file name // parse file name
object = ((File) object).getName(); object = ((File) object).getName();
@ -50,7 +50,7 @@ public class DateMetric implements SimilarityMetric {
} }
protected Date match(CharSequence name) { public Date match(CharSequence name) {
for (DatePattern pattern : patterns) { for (DatePattern pattern : patterns) {
Date match = pattern.match(name); Date match = pattern.match(name);

View File

@ -64,7 +64,7 @@ public enum EpisodeMetrics implements SimilarityMetric {
@Override @Override
protected Date parse(Object object) { public Date parse(Object object) {
if (object instanceof Movie) { if (object instanceof Movie) {
return null; return null;
} }

View File

@ -433,7 +433,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider {
BannerMirror, BannerMirror,
banner, banner,
fanart, 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() { public String getStatus() {
// e.g. Continuing // e.g. Continuing
return get(SeriesProperty.Status); return get(SeriesProperty.Status);
} }
public URL getBannerMirrorUrl() throws MalformedURLException { public URL getBannerMirrorUrl() {
return new URL(get(BannerProperty.BannerMirror)); try {
return new URL(get(BannerProperty.BannerMirror));
} catch (Exception e) {
return null;
}
} }
public URL getBannerUrl() throws MalformedURLException { 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 { public URL getFanartUrl() {
return new URL(getBannerMirrorUrl(), get(SeriesProperty.fanart)); try {
return new URL(getBannerMirrorUrl(), get(SeriesProperty.fanart));
} catch (Exception e) {
return null;
}
} }
public URL getPosterUrl() throws MalformedURLException { 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;
}
} }

View File

@ -1,77 +1,101 @@
// filebot -script "http://filebot.sourceforge.net/data/shell/banners.groovy" -trust-script /path/to/media/ // 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 * Fetch series and season banners for all tv shows
*/ */
import static net.sourceforge.filebot.WebServices.*
def fetchBanner(outputFile, series, bannerType, bannerType2, season = null) {
def fetchBanner(outputDir, outputName, series, bannerType, bannerType2, season = null) {
// select and fetch banner // select and fetch banner
def banner = TheTVDB.getBanner(series, bannerType, bannerType2, season, Locale.ENGLISH, 0) def banner = TheTVDB.getBanner(series, bannerType, bannerType2, season, Locale.ENGLISH, 0)
if (banner == null) { if (banner == null) {
println "Banner not found: $outputName" println "Banner not found: $outputFile"
return null return null
} }
println "Fetching banner $banner" println "Fetching banner $banner"
return banner.url.saveAs(new File(outputDir, outputName + ".jpg")) return banner.url.saveAs(outputFile)
} }
def fetchNfo(outputDir, outputName, series) { def fetchNfo(outputFile, series) {
def info = TheTVDB.getSeriesInfo(series, Locale.ENGLISH) TheTVDB.getSeriesInfo(series, Locale.ENGLISH).applyXmlTemplate('''
println "Writing nfo $info" <tvshow xmlns:gsp='http://groovy.codehaus.org/2005/gsp'>
<title>$name</title>
new File(outputDir, outputName + ".nfo").withWriter{ out -> <year>$firstAired.year</year>
out.println("Name: $info.name") <rating>$rating</rating>
out.println("IMDb: http://www.imdb.com/title/tt${info.imdbId.pad(7)}") <votes>$ratingCount</votes>
out.println("Actors: ${info.actors.join(', ')}") <plot>$overview</plot>
out.println("Genere: ${info.genre.join(', ')}") <runtime>$runtime</runtime>
out.println("Language: ${info.language.displayName}") <mpaa>$contentRating</mpaa>
out.println("Overview: $info.overview") <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) { def fetchSeriesBannersAndNfo(seriesDir, seasonDir, series, season) {
println "Fetch nfo and banners for $series / Season $seasons" println "Fetch nfo and banners for $series / Season $season"
// fetch nfo // fetch nfo
fetchNfo(dir, series.name, series) fetchNfo(seriesDir['tvshow.nfo'], series)
// fetch series banner, fanart, posters, etc // fetch series banner, fanart, posters, etc
fetchBanner(dir, "folder", series, "poster", "680x1000") fetchBanner(seriesDir['folder.jpg'], series, "poster", "680x1000")
fetchBanner(dir, "banner", series, "series", "graphical") fetchBanner(seriesDir['banner.jpg'], series, "series", "graphical")
// fetch highest resolution fanart // 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 // fetch season banners
seasons.each { s -> if (seasonDir != seriesDir) {
fetchBanner(dir, "folder-S${s.pad(2)}", series, "season", "season", s) fetchBanner(seasonDir["folder.jpg"], series, "season", "season", season)
fetchBanner(dir, "banner-S${s.pad(2)}", series, "season", "seasonwide", s) fetchBanner(seasonDir["banner.jpg"], series, "season", "seasonwide", season)
} }
} }
args.eachMediaFolder() { dir -> def jobs = args.getFolders().findResults { dir ->
println "Processing $dir" def videos = dir.listFiles{ it.isVideo() }
def videoFiles = dir.listFiles{ it.isVideo() } if (videos.isEmpty()) {
return null
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 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()) { if (options.isEmpty()) {
println "TV Series not found: $seriesName" println "TV Series not found: $query"
return; 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)

View File

@ -1,5 +1,9 @@
// filebot -script "http://filebot.sourceforge.net/data/shell/housekeeping.groovy" <folder> // 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 * Watch folder for new tv shows and automatically
* move/rename new episodes into a predefined folder structure * move/rename new episodes into a predefined folder structure
@ -8,11 +12,8 @@
// check for new media files once every 5 seconds // check for new media files once every 5 seconds
def updateFrequency = 5 * 1000; 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 // 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 // spawn daemon thread
Thread.startDaemon { Thread.startDaemon {

View File

@ -1,8 +1,10 @@
// filebot -script "http://filebot.sourceforge.net/data/shell/rsam.groovy" <options> <folder> // 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) * Rename anime, tv shows or movies (assuming each folder represents one item)

View File

@ -1,16 +1,17 @@
// Settings // EXPERIMENTAL // HERE THERE BE DRAGONS
def episodeDir = "X:/in/TV"
def movieDir = "X:/in/Movies"
def episodeFormat = "X:/out/TV/{n}{'/Season '+s}/{episode}" // PERSONALIZED SETTINGS
def movieFormat = "X:/out/Movies/{movie}/{movie}" 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 // run cmdline unrar (require -trust-script) on multi-volume rar files
[episodeDir, movieDir].getFiles().findAll { [episodeDir, movieDir].getFiles().findAll { it =~ /[.]part01[.]rar$/ || (it =~ /[.]rar$/ && !(it =~ /[.]part\d{2}[.]rar$/)) }.each { rar ->
it =~ /[.]part01[.]rar$/ || (it =~ /[.]rar$/ && !(it =~ /[.]part\d{2}[.]rar$/))
}.each { rar ->
// new layout: foo.part1.rar, foo.part2.rar // new layout: foo.part1.rar, foo.part2.rar
// old layout: foo.rar, foo.r00, foo.r01 // old layout: foo.rar, foo.r00, foo.r01
boolean partLayout = (rar =~ /[.]part01[.]rar/) boolean partLayout = (rar =~ /[.]part01[.]rar/)
@ -41,8 +42,9 @@ def incomplete(f) { f =~ /[.]chunk|[.]part$/ }
/* /*
* Fetch subtitles and sort into folders * Fetch subtitles and sort into folders
*/ */
episodeDir.eachMediaFolder() { dir -> episodeDir.getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it.isVideo() } }.each{ dir ->
def files = dir.listFiles { !incomplete(it) } println "Processing $dir"
def files = dir.listFiles{ it.isVideo() }
// fetch subtitles // fetch subtitles
files += getSubtitles(file:files) files += getSubtitles(file:files)
@ -51,8 +53,9 @@ episodeDir.eachMediaFolder() { dir ->
rename(file:files, db:'TVRage', format:episodeFormat) rename(file:files, db:'TVRage', format:episodeFormat)
} }
movieDir.eachMediaFolder() { dir -> movieDir.getFolders{ !it.hasFile{ incomplete(it) } && it.hasFile{ it.isVideo() } }.each{ dir ->
def files = dir.listFiles { !incomplete(it) } println "Processing $dir"
def files = dir.listFiles{ it.isVideo() }
// fetch subtitles // fetch subtitles
files += getSubtitles(file:files) files += getSubtitles(file:files)

View File

@ -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) // watch folders and print files that were added/modified (requires Java 7)
def watchman = args.watch { changes -> def watchman = args.watch { changes ->
println "Processing $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 // process after 10 minutes without any changes to the folder