* 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.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.jar.Manifest;
@ -138,16 +137,20 @@ 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()));
}

View File

@ -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;
@ -96,7 +97,7 @@ 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;
}

View File

@ -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.*"));

View File

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

View File

@ -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,12 +13,12 @@ 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);
}
@ -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
public String toString() {
// 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.
*
*/
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);
}
}
@ -104,7 +84,7 @@ public class AssociativeScriptObject implements GroovyObject {
@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();
@ -114,14 +94,13 @@ public class AssociativeScriptObject implements GroovyObject {
@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>>() {
@Override
public Iterator<Entry<String, Object>> iterator() {
@SuppressWarnings("unchecked")
Iterator<Entry<String, Object>> iterator = (Iterator) lookup.values().iterator();
return iterator;
@Override
public Iterator<Entry<Object, Object>> iterator() {
return (Iterator) lookup.values().iterator();
}

View File

@ -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) }
/**

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)) {
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()));

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) {
// parse file name
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) {
Date match = pattern.match(name);

View File

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

View File

@ -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 {
public URL getBannerMirrorUrl() {
try {
return new URL(get(BannerProperty.BannerMirror));
} catch (Exception e) {
return null;
}
}
public URL getBannerUrl() throws MalformedURLException {
try {
return new URL(getBannerMirrorUrl(), get(SeriesProperty.banner));
} catch (Exception e) {
return null;
}
}
public URL getFanartUrl() throws MalformedURLException {
public URL getFanartUrl() {
try {
return new URL(getBannerMirrorUrl(), get(SeriesProperty.fanart));
} catch (Exception e) {
return null;
}
}
public URL getPosterUrl() throws MalformedURLException {
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/
// 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)

View File

@ -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 {

View File

@ -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)

View File

@ -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)

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)
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