1
0
mirror of https://github.com/mitb-archive/filebot synced 2024-12-23 00:08:51 -05:00

+ Groovy engine extensions rewrite complete :)

This commit is contained in:
Reinhard Pointner 2014-04-18 19:41:39 +00:00
parent 2ba959e2b5
commit ca3fc8f3fa
12 changed files with 302 additions and 801 deletions

View File

@ -1,5 +1,4 @@
import org.tukaani.xz.*
import net.sourceforge.filebot.media.*
/* ------------------------------------------------------------------------- */
@ -92,6 +91,16 @@ def treeSort(list, keyFunction) {
return sorter.values()
}
def csv(f, delim, keyIndex, valueIndex) {
def values = [:]
if (f.isFile()) {
f.splitEachLine(delim, 'UTF-8') { line ->
values.put(line[keyIndex], tryQuietly{ line[valueIndex] })
}
}
return values
}
/* ------------------------------------------------------------------------- */
@ -127,7 +136,7 @@ def tmdb = omdb.findResults{ m ->
def row = [sync, m[0].pad(7), 0, m[2], m[1]]
try {
def info = net.sourceforge.filebot.WebServices.TMDb.getMovieInfo("tt${m[0]}", Locale.ENGLISH, true, false)
def info = WebServices.TheMovieDB.getMovieInfo("tt${m[0]}", Locale.ENGLISH, true, false)
def names = [info.name, info.originalName] + info.alternativeTitles
if (info.released != null) {
row = [sync, m[0].pad(7), info.id.pad(7), info.released.year] + names
@ -172,22 +181,22 @@ if (tvdb_txt.exists()) {
}
}
def tvdb_updates = new File('updates_all.xml').text.xml.'**'.Series.findResults{ s -> tryQuietly{ [id:s.id.text() as Integer, time:s.time.text() as Integer] } }
def tvdb_updates = new XmlSlurper().parse('updates_all.xml' as File).Series.findResults{ s -> tryQuietly{ [id:s.id.text() as Integer, time:s.time.text() as Integer] } }
tvdb_updates.each{ update ->
if (tvdb[update.id] == null || update.time > tvdb[update.id][0]) {
try {
retry(2, 500) {
def xml = new URL("http://thetvdb.com/api/BA864DEE427E384A/series/${update.id}/en.xml").fetch().text.xml
def imdbid = xml.'**'.IMDB_ID.text()
def tvdb_name = xml.'**'.SeriesName.text()
def xml = new XmlSlurper().parse("http://thetvdb.com/api/BA864DEE427E384A/series/${update.id}/en.xml")
def imdbid = xml.Series.IMDB_ID.text()
def tvdb_name = xml.Series.SeriesName.text()
def rating = tryQuietly{ xml.'**'.Rating.text().toFloat() }
def votes = tryQuietly{ xml.'**'.RatingCount.text().toInteger() }
def rating = tryQuietly{ xml.Series.Rating.text().toFloat() }
def votes = tryQuietly{ xml.Series.RatingCount.text().toInteger() }
def imdb_name = _guarded{
def imdb_name = tryLogCatch{
if (imdbid =~ /tt(\d+)/) {
def dom = IMDb.parsePage(IMDb.getMoviePageLink(imdbid.match(/tt(\d+)/) as int).toURL())
return net.sourceforge.filebot.util.XPathUtilities.selectString("//META[@property='og:title']/@content", dom)
return XPathUtilities.selectString("//META[@property='og:title']/@content", dom)
}
}
def data = [update.time, update.id, imdbid, tvdb_name ?: '', imdb_name ?: '', rating ?: 0, votes ?: 0]
@ -271,7 +280,7 @@ pack(thetvdb_out, thetvdb_txt)
// BUILD anidb index
def anidb = new net.sourceforge.filebot.web.AnidbClient('filebot', 4).getAnimeTitles()
def anidb = new AnidbClient('filebot', 4).getAnimeTitles()
def anidb_index = anidb.findResults{
def names = it.effectiveNames*.replaceAll(/\s+/, ' ')*.trim()*.replaceAll(/['`´ʻ]+/, /'/)

View File

@ -115,7 +115,7 @@ Section MAIN
DetailPrint "Clearing cache and temporary files..."
nsExec::Exec `"C:\Program Files\FileBot\filebot.exe" -clear-cache`
DetailPrint "Initializing Cache..."
nsExec::Exec `"C:\Program Files\FileBot\filebot.exe" -script "g:net.sourceforge.filebot.media.MediaDetection.warmupCachedResources()"`
nsExec::Exec `"C:\Program Files\FileBot\filebot.exe" -script "g:MediaDetection.warmupCachedResources()"`
${else}
DetailPrint "msiexec error $MSI_STATUS"
DetailPrint "Install failed. Please download the .msi package manually."

View File

@ -2,7 +2,6 @@ package net.sourceforge.filebot.cli;
import groovy.lang.GroovyClassLoader;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Map;
import java.util.ResourceBundle;
@ -38,13 +37,6 @@ public class ScriptShell {
// setup script context
engine.getContext().setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
// import additional functions into the shell environment
// TODO remove
// engine.eval(new InputStreamReader(ExpressionFormat.class.getResourceAsStream("ExpressionFormat.lib.groovy")));
bindings.put("_shell", this);
bindings.put("_cli", new CmdlineOperations());
engine.eval(new InputStreamReader(ScriptShell.class.getResourceAsStream("ScriptShell.lib.groovy")));
}
public ScriptEngine createScriptEngine() {

View File

@ -1,432 +0,0 @@
// File selector methods
import static groovy.io.FileType.*
import static groovy.io.FileVisitResult.*
// MediaDetection
import net.sourceforge.filebot.media.*
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) }
File.metaClass.isAudio = { _types.getFilter("audio").accept(delegate) }
File.metaClass.isSubtitle = { _types.getFilter("subtitle").accept(delegate) }
File.metaClass.isVerification = { _types.getFilter("verification").accept(delegate) }
File.metaClass.isArchive = { _types.getFilter("archive").accept(delegate) }
File.metaClass.isDisk = { (delegate.isDirectory() && MediaDetection.isDiskFolder(delegate)) || (delegate.isFile() && _types.getFilter("video/iso").accept(delegate) && MediaDetection.isVideoDiskFile(delegate)) }
File.metaClass.getDir = { getParentFile() }
File.metaClass.hasFile = { c -> isDirectory() && listFiles().find(c) }
String.metaClass.getFiles = { c -> new File(delegate).getFiles(c) }
File.metaClass.getFiles = { c -> if (delegate.isFile()) return [delegate]; def files = []; traverse(type:FILES, visitRoot:true) { files += it }; return c ? files.findAll(c).sort() : files.sort() }
List.metaClass.getFiles = { c -> findResults{ it.getFiles(c) }.flatten().unique() }
String.metaClass.getFolders = { c -> new File(delegate).getFolders(c) }
File.metaClass.getFolders = { c -> def folders = []; traverse(type:DIRECTORIES, visitRoot:true) { folders += it }; return c ? folders.findAll(c).sort() : folders.sort() }
List.metaClass.getFolders = { c -> findResults{ it.getFolders(c) }.flatten().unique() }
File.metaClass.listFolders = { c -> delegate.listFiles().findAll{ it.isDirectory() } }
File.metaClass.getMediaFolders = { def folders = []; traverse(type:DIRECTORIES, visitRoot:true, preDir:{ it.isDisk() ? SKIP_SUBTREE : CONTINUE }) { folders += it }; folders.findAll{ it.hasFile{ it.isVideo() } || it.isDisk() }.sort() }
String.metaClass.eachMediaFolder = { c -> new File(delegate).eachMediaFolder(c) }
File.metaClass.eachMediaFolder = { c -> delegate.getMediaFolders().each(c) }
List.metaClass.eachMediaFolder = { c -> delegate.findResults{ it.getMediaFolders() }.flatten().unique().each(c) }
// File utility methods
import static net.sourceforge.filebot.util.FileUtilities.*
File.metaClass.getNameWithoutExtension = { getNameWithoutExtension(delegate.getName()) }
File.metaClass.getPathWithoutExtension = { new File(delegate.getParentFile(), getNameWithoutExtension(delegate.getName())).getPath() }
File.metaClass.getExtension = { getExtension(delegate) }
File.metaClass.hasExtension = { String... ext -> hasExtension(delegate, ext) }
File.metaClass.isDerived = { f -> isDerived(delegate, f) }
File.metaClass.validateFileName = { validateFileName(delegate) }
File.metaClass.validateFilePath = { validateFilePath(delegate) }
File.metaClass.moveTo = { f -> moveRename(delegate, f as File) }
File.metaClass.copyAs = { f -> copyAs(delegate, f) }
File.metaClass.copyTo = { dir -> copyAs(delegate, new File(dir, delegate.getName())) }
File.metaClass.getXattr = { new net.sourceforge.filebot.MetaAttributeView(delegate) }
File.metaClass.relativize = { f -> delegate.canonicalFile.toPath().relativize(f.canonicalFile.toPath()).toFile() }
List.metaClass.mapByFolder = { mapByFolder(delegate) }
List.metaClass.mapByExtension = { mapByExtension(delegate) }
String.metaClass.getNameWithoutExtension = { getNameWithoutExtension(delegate) }
String.metaClass.getExtension = { getExtension(delegate) }
String.metaClass.hasExtension = { String... ext -> hasExtension(delegate, ext) }
String.metaClass.validateFileName = { validateFileName(delegate) }
// helper for enforcing filename length limits, e.g. truncate filename but keep extension
String.metaClass.truncateFileName = { int limit = 255 -> def ext = getExtension(delegate); def name = getNameWithoutExtension(delegate); return name.substring(0, Math.min(limit - (ext ? 1+ext.length() : 0), name.length())) + (ext ? '.'+ext : '') }
// helper for simplifying strings
String.metaClass.normalizePunctuation = { net.sourceforge.filebot.similarity.Normalization.normalizePunctuation(delegate) }
// 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{ c -> _guarded { c.get() } }
}
// Web and File IO helpers
import java.nio.ByteBuffer
import java.nio.charset.Charset
import static net.sourceforge.filebot.web.WebRequest.*
URL.metaClass.fetch = { fetch(delegate) }
ByteBuffer.metaClass.getText = { csn = "utf-8" -> Charset.forName(csn).decode(delegate.duplicate()).toString() }
ByteBuffer.metaClass.getHtml = { csn = "utf-8" -> new XmlParser(new org.cyberneko.html.parsers.SAXParser()).parseText(delegate.getText(csn)) }
String.metaClass.getHtml = { new XmlParser(new org.cyberneko.html.parsers.SAXParser()).parseText(delegate) }
String.metaClass.getXml = { new XmlParser().parseText(delegate) }
URL.metaClass.get = { delegate.getText() }
URL.metaClass.post = { Map parameters, requestParameters = null -> post(delegate, parameters, requestParameters) }
URL.metaClass.post = { byte[] data, contentType = 'application/octet-stream', requestParameters = null -> post(delegate, data, contentType, requestParameters) }
URL.metaClass.post = { String text, contentType = 'text/plain', csn = 'utf-8', requestParameters = null -> post(delegate, text.getBytes(csn), contentType, requestParameters) }
ByteBuffer.metaClass.saveAs = { f -> f = f as File; f = f.absoluteFile; f.parentFile.mkdirs(); writeFile(delegate.duplicate(), f); f }
URL.metaClass.saveAs = { f -> fetch(delegate).saveAs(f) }
String.metaClass.saveAs = { f, csn = "utf-8" -> Charset.forName(csn).encode(delegate).saveAs(f) }
def telnet(host, int port, csn = 'utf-8', Closure handler) {
def socket = new Socket(host, port)
try {
handler.call(new PrintStream(socket.outputStream, true, csn), socket.inputStream.newReader(csn))
} finally {
socket.close()
}
}
// json-io helpers
import com.cedarsoftware.util.io.*
Object.metaClass.objectToJson = { JsonWriter.objectToJson(delegate) }
String.metaClass.jsonToObject = { JsonReader.jsonToJava(delegate) }
String.metaClass.jsonToMap = { JsonReader.jsonToMaps(delegate) }
// Template Engine helpers
import groovy.text.XmlTemplateEngine
import groovy.text.GStringTemplateEngine
import net.sourceforge.filebot.format.PropertyBindings
import net.sourceforge.filebot.format.UndefinedObject
Object.metaClass.applyXml = { template -> new XmlTemplateEngine("\t", false).createTemplate(template).make(new PropertyBindings(delegate, new UndefinedObject(""))).toString() }
Object.metaClass.applyText = { template -> new GStringTemplateEngine().createTemplate(template).make(new PropertyBindings(delegate, new UndefinedObject(""))).toString() }
// MarkupBuilder helper
import groovy.xml.MarkupBuilder
def XML(bc) {
def out = new StringWriter()
def xmb = new MarkupBuilder(out)
xmb.omitNullAttributes = true
xmb.omitEmptyAttributes = false
xmb.expandEmptyElements= false
bc.rehydrate(bc.delegate, xmb, xmb).call() // call closure in MarkupBuilder context
return out.toString()
}
// Shell helper
import com.sun.jna.Platform
def execute(Object... args) {
def cmd = (args as List).flatten().collect{ it as String }
if (Platform.isWindows()) {
// normalize file separator for windows and run with cmd so any executable in PATH will just work
cmd = ['cmd', '/c'] + cmd
} else if (cmd.size() == 1) {
// make unix shell parse arguments
cmd = ['sh', '-c'] + cmd
}
// run command and print output
def process = cmd.execute()
process.waitForProcessOutput(System.out, System.err)
return process.exitValue()
}
// WatchService helper
import net.sourceforge.filebot.cli.FolderWatchService
def createWatchService(Closure callback, List folders, boolean watchTree) {
// sanity check
folders.find{ if (!it.isDirectory()) throw new Exception("Must be a folder: " + it) }
// create watch service and setup callback
def watchService = new FolderWatchService(true) {
@Override
def void processCommitSet(File[] fileset, File dir) {
callback(fileset.toList())
}
}
// collect updates for 500 ms and then batch process
watchService.setCommitDelay(500)
watchService.setCommitPerFolder(watchTree)
// start watching given files
folders.each { dir -> _guarded { watchService.watchFolder(dir) } }
return watchService
}
File.metaClass.watch = { c -> createWatchService(c, [delegate], true) }
List.metaClass.watch = { c -> createWatchService(c, delegate, true) }
// FileBot MetaAttributes helpers
import net.sourceforge.filebot.media.*
import net.sourceforge.filebot.format.*
import net.sourceforge.filebot.web.*
File.metaClass.getMetadata = { net.sourceforge.filebot.Settings.useExtendedFileAttributes() ? new MetaAttributes(delegate) : null }
File.metaClass.getMediaBinding = { new MediaBindingBean(delegate.metadata, delegate, null) }
Movie.metaClass.getMediaBinding = Episode.metaClass.getMediaBinding = { new MediaBindingBean(delegate, null, null) }
// Complete or session rename history
def getRenameLog(complete = false) {
def spooler = net.sourceforge.filebot.HistorySpooler.getInstance()
def history = complete ? spooler.completeHistory : spooler.sessionHistory
return history.sequences*.elements.flatten().collectEntries{ [new File(it.dir, it.from), new File(it.to).isAbsolute() ? new File(it.to) : new File(it.dir, it.to)] }
}
// Season / Episode helpers
import net.sourceforge.filebot.similarity.*
def stripReleaseInfo(name, strict = true) {
def result = MediaDetection.stripReleaseInfo([name], strict)
return result.size() > 0 ? result[0] : null
}
def isEpisode(path, strict = true) {
def input = path instanceof File ? path.name : path.toString()
return MediaDetection.isEpisode(input, strict)
}
def isStructureRoot(path) {
return MediaDetection.isStructureRoot(path as File)
}
def guessMovieFolder(File path) {
return MediaDetection.guessMovieFolder(path)
}
def parseEpisodeNumber(path, strict = true) {
def input = path instanceof File ? path.name : path.toString()
def sxe = MediaDetection.parseEpisodeNumber(input, strict)
return sxe == null || sxe.isEmpty() ? null : sxe[0]
}
def parseDate(path) {
def input = path instanceof File ? path.name : path.toString()
return MediaDetection.parseDate(input)
}
def detectSeriesName(files, boolean useSeriesIndex = true, boolean useAnimeIndex = false, Locale locale = Locale.ENGLISH) {
def names = MediaDetection.detectSeriesNames(files instanceof Collection ? files : [files as File], useSeriesIndex, useAnimeIndex, locale)
return names == null || names.isEmpty() ? null : names.toList()[0]
}
def detectMovie(File file, strict = true, queryLookupService = TheMovieDB, hashLookupService = OpenSubtitles, locale = Locale.ENGLISH) {
// 1. xattr
def m = tryQuietly{ file.metadata.object as Movie }
if (m != null)
return m
// 2. perfect filename match
m = MediaDetection.matchMovieName(file.listPath(4).reverse().findResults{ it.name ?: null }, true, 0)
if (m != null && m.size() > 0)
return m[0]
// 3. run full-fledged movie detection
m = MediaDetection.detectMovie(file, hashLookupService, queryLookupService, locale, strict)
if (m != null && m.size() > 0)
return m[0]
return null
}
def matchMovie(String filename, strict = true, maxStartIndex = 0) {
def movies = MediaDetection.matchMovieName([filename], strict, maxStartIndex)
return movies == null || movies.isEmpty() ? null : movies.toList()[0]
}
def similarity(o1, o2) {
return new NameSimilarityMetric().getSimilarity(o1, o2)
}
List.metaClass.sortBySimilarity = { prime, Closure toStringFunction = { obj -> obj.toString() } ->
def simetric = new NameSimilarityMetric()
return delegate.sort{ a, b -> simetric.getSimilarity(toStringFunction(b), prime).compareTo(simetric.getSimilarity(toStringFunction(a), prime)) }
}
// call scripts
def executeScript(String input, Map bindings = [:], Object... args) {
// apply parent script defines
def parameters = new javax.script.SimpleBindings()
// initialize default parameter
parameters.putAll(_def)
parameters.putAll(bindings)
parameters.put('args', args.toList().flatten().findResults{ it as File })
// run given script
_shell.runScript(input, parameters)
}
def include(String input, Map bindings = [:], Object... args) {
// run given script and catch exceptions
_guarded { executeScript(input, bindings, args) }
}
// CLI bindings
def rename(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.rename(_files(args), _renameFunction(args.action), args.conflict as String, args.output as String, args.format as String, args.db as String, args.query as String, args.order as String, args.filter as String, args.lang as String, args.strict as Boolean) }
}
}
def getSubtitles(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.getSubtitles(_files(args), args.db as String, args.query as String, args.lang as String, args.output as String, args.encoding as String, args.format as String, args.strict as Boolean) }
}
}
def getMissingSubtitles(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.getMissingSubtitles(_files(args), args.db as String, args.query as String, args.lang as String, args.output as String, args.encoding as String, args.format as String, args.strict as Boolean) }
}
}
def check(args) {
synchronized (_cli) {
_guarded { _cli.check(_files(args)) }
}
}
def compute(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.compute(_files(args), args.output as String, args.encoding as String) }
}
}
def extract(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.extract(_files(args), args.output as String, args.conflict as String, args.filter instanceof Closure ? args.filter as FileFilter : null, args.forceExtractAll != null ? args.forceExtractAll : false) }
}
}
def fetchEpisodeList(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.fetchEpisodeList(args.query as String, args.format as String, args.db as String, args.order as String, args.lang as String) }
}
}
def getMediaInfo(args) { args = _defaults(args)
synchronized (_cli) {
_guarded { _cli.getMediaInfo(args.file as File, args.format as String) }
}
}
/**
* Resolve folders/files to lists of one or more files
*/
def _files(args) {
def files = [];
if (args.folder) {
(args.folder as File).traverse(type:FILES, maxDepth:0) { files += it }
}
if (args.file) {
if (args.file instanceof Iterable || args.file instanceof Object[]) {
files += args.file as List
} else {
files += args.file as File
}
}
// ignore invalid input
return files.flatten().findResults{ it as File }
}
// allow Groovy to hook into rename interface
import net.sourceforge.filebot.*
def _renameFunction(fn) {
if (fn instanceof String)
return StandardRenameAction.forName(fn)
if (fn instanceof Closure)
return [rename:{ from, to -> def result = fn.call(from, to); result instanceof File ? result : to }, toString:{'CLOSURE'}] as RenameAction
return fn as RenameAction
}
/**
* Fill in default values from cmdline arguments
*/
def _defaults(args) {
['action', 'conflict', 'query', 'filter', 'format', 'db', 'order', 'lang', 'output', 'encoding'].each{ k ->
args[k] = args.containsKey(k) ? args[k] : _args[k]
}
args.strict = args.strict != null ? args.strict : !_args.nonStrict // invert strict/non-strict
return args
}
/**
* Catch and log exceptions thrown by the closure
*/
def _guarded(c) {
try {
return c.call()
} catch (Throwable e) {
_log.severe("${e.class.simpleName}: ${e.message}")
return null
}
}
/**
* Same as the above but without logging anything
*/
def tryQuietly(c) {
try {
return c.call()
} catch (Throwable e) {
return null
}
}
/**
* Retry given closure until it returns successfully (indefinitely by default)
*/
def retry(n = -1, wait = 0, quiet = false, c) {
for(int i = 0; n < 0 || i <= n; i++) {
try {
return c.call()
} catch(Throwable e) {
if (i >= 0 && i >= n) {
throw e
} else if (!quiet) {
_log.warning("retry $i: ${e.class.simpleName}: ${e.message}")
}
sleep(wait)
}
}
}

View File

@ -1,3 +1,3 @@
scriptBaseClass: net.sourceforge.filebot.cli.ScriptShellBaseClass
starImport: net.sourceforge.filebot, net.sourceforge.filebot.hash, net.sourceforge.filebot.media, net.sourceforge.filebot.mediainfo, net.sourceforge.filebot.similarity, net.sourceforge.filebot.subtitle, net.sourceforge.filebot.torrent, net.sourceforge.filebot.web, net.sourceforge.filebot.util, groovy.io, groovy.xml, groovy.json, org.jsoup, java.nio.file, java.nio.file.attribute, java.util.regex
starImport: net.sourceforge.filebot, net.sourceforge.filebot.hash, net.sourceforge.filebot.media, net.sourceforge.filebot.mediainfo, net.sourceforge.filebot.similarity, net.sourceforge.filebot.subtitle, net.sourceforge.filebot.torrent, net.sourceforge.filebot.web, net.sourceforge.filebot.util, groovy.io, groovy.xml, groovy.json, java.nio.file, java.nio.file.attribute, java.util.regex
starStaticImport: net.sourceforge.filebot.WebServices, net.sourceforge.filebot.media.MediaDetection, net.sourceforge.filebot.format.ExpressionFormatFunctions

View File

@ -1,8 +1,10 @@
package net.sourceforge.filebot.cli;
import static java.util.Collections.*;
import static java.util.EnumSet.*;
import static net.sourceforge.filebot.Settings.*;
import static net.sourceforge.filebot.cli.CLILogging.*;
import static net.sourceforge.filebot.util.StringUtilities.*;
import groovy.lang.Closure;
import groovy.lang.MissingPropertyException;
import groovy.lang.Script;
@ -10,6 +12,7 @@ import groovy.xml.MarkupBuilder;
import java.io.Console;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
@ -17,7 +20,7 @@ import java.io.StringWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -28,29 +31,31 @@ import javax.script.Bindings;
import javax.script.SimpleBindings;
import net.sourceforge.filebot.HistorySpooler;
import net.sourceforge.filebot.RenameAction;
import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.StandardRenameAction;
import net.sourceforge.filebot.WebServices;
import net.sourceforge.filebot.format.AssociativeScriptObject;
import net.sourceforge.filebot.media.MediaDetection;
import net.sourceforge.filebot.media.MetaAttributes;
import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher.SxE;
import net.sourceforge.filebot.util.FileUtilities;
import net.sourceforge.filebot.web.Movie;
import org.codehaus.groovy.runtime.StackTraceUtils;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
import com.sun.jna.Platform;
public abstract class ScriptShellBaseClass extends Script {
public ScriptShellBaseClass() {
System.out.println(this);
}
private Map<String, ?> defaultValues;
private Map<String, Object> defaultValues;
public void setDefaultValues(Map<String, ?> values) {
this.defaultValues = values;
this.defaultValues = new LinkedHashMap<String, Object>(values);
}
public Map<String, ?> getDefaultValues() {
public Map<String, Object> getDefaultValues() {
return defaultValues;
}
@ -64,7 +69,7 @@ public abstract class ScriptShellBaseClass extends Script {
return defaultValues.get(property);
}
// can't use default value, rethrow exception
// can't use default value, rethrow original exception
throw e;
}
}
@ -104,7 +109,7 @@ public abstract class ScriptShellBaseClass extends Script {
}
}
public Object tryLoudly(Closure<?> c) {
public Object tryLogCatch(Closure<?> c) {
try {
return c.call();
} catch (Exception e) {
@ -114,7 +119,10 @@ public abstract class ScriptShellBaseClass extends Script {
}
public void printException(Throwable t) {
CLILogger.severe(String.format("%s: %s", t.getClass().getSimpleName(), t.getMessage()));
CLILogger.severe(String.format("%s: %s", t.getClass().getName(), t.getMessage()));
// DEBUG
StackTraceUtils.deepSanitize(t).printStackTrace();
}
public void die(String message) throws Throwable {
@ -170,8 +178,21 @@ public abstract class ScriptShellBaseClass extends Script {
}
public String detectSeriesName(Object files) throws Exception {
List<String> names = MediaDetection.detectSeriesNames(FileUtilities.asFileList(files), true, false, Locale.ENGLISH);
return names.isEmpty() ? null : names.get(0);
return detectSeriesName(files, true, false);
}
public String detectAnimeName(Object files) throws Exception {
return detectSeriesName(files, false, true);
}
public String detectSeriesName(Object files, boolean useSeriesIndex, boolean useAnimeIndex) throws Exception {
List<String> names = MediaDetection.detectSeriesNames(FileUtilities.asFileList(files), useSeriesIndex, useAnimeIndex, Locale.ENGLISH);
return names == null || names.isEmpty() ? null : names.get(0);
}
public static SxE parseEpisodeNumber(Object object) {
List<SxE> matches = MediaDetection.parseEpisodeNumber(object.toString(), true);
return matches == null || matches.isEmpty() ? null : matches.get(0);
}
public Movie detectMovie(File file, boolean strict) {
@ -237,27 +258,216 @@ public abstract class ScriptShellBaseClass extends Script {
}
}
private enum OptionName {
action, conflict, query, filter, format, db, order, lang, output, encoding, strict
/**
* Retry given closure until it returns successfully (indefinitely if -1 is passed as retry count)
*/
public Object retry(int retryCountLimit, int retryWaitTime, Closure<?> c) throws InterruptedException {
for (int i = 0; retryCountLimit < 0 || i <= retryCountLimit; i++) {
try {
return c.call();
} catch (Exception e) {
if (i >= 0 && i >= retryCountLimit) {
throw e;
}
Thread.sleep(retryWaitTime);
}
}
return null;
}
private Map<OptionName, Object> withDefaultOptions(Map<String, ?> map) throws Exception {
Map<OptionName, Object> options = new EnumMap<OptionName, Object>(OptionName.class);
private enum Option {
action, conflict, query, filter, format, db, order, lang, output, encoding, strict, forceExtractAll
}
for (Entry<String, ?> it : map.entrySet()) {
options.put(OptionName.valueOf(it.getKey()), it.getValue());
private static final CmdlineInterface cli = new CmdlineOperations();
public List<File> rename(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
RenameAction action = getRenameFunction(option.get(Option.action));
boolean strict = DefaultTypeTransformation.castToBoolean(option.get(Option.strict));
synchronized (cli) {
try {
return cli.rename(input, action, asString(option.get(Option.conflict)), asString(option.get(Option.output)), asString(option.get(Option.format)), asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.order)), asString(option.get(Option.filter)), asString(option.get(Option.lang)), strict);
} catch (Exception e) {
printException(e);
return null;
}
}
}
public List<File> getSubtitles(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
boolean strict = DefaultTypeTransformation.castToBoolean(option.get(Option.strict));
synchronized (cli) {
try {
return cli.getSubtitles(input, asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.lang)), asString(option.get(Option.output)), asString(option.get(Option.encoding)), asString(option.get(Option.format)), strict);
} catch (Exception e) {
printException(e);
return null;
}
}
}
public List<File> getMissingSubtitles(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
boolean strict = DefaultTypeTransformation.castToBoolean(option.get(Option.strict));
synchronized (cli) {
try {
return cli.getMissingSubtitles(input, asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.lang)), asString(option.get(Option.output)), asString(option.get(Option.encoding)), asString(option.get(Option.format)), strict);
} catch (Exception e) {
printException(e);
return null;
}
}
}
public boolean check(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
synchronized (cli) {
try {
return cli.check(input);
} catch (Exception e) {
printException(e);
return false;
}
}
}
public File compute(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
synchronized (cli) {
try {
return cli.compute(input, asString(option.get(Option.output)), asString(option.get(Option.encoding)));
} catch (Exception e) {
printException(e);
return null;
}
}
}
public List<File> extract(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
FileFilter filter = (FileFilter) DefaultTypeTransformation.castToType(option.get(Option.filter), FileFilter.class);
boolean forceExtractAll = DefaultTypeTransformation.castToBoolean(option.get(Option.forceExtractAll));
synchronized (cli) {
try {
return cli.extract(input, asString(option.get(Option.output)), asString(option.get(Option.conflict)), filter, forceExtractAll);
} catch (Exception e) {
printException(e);
return null;
}
}
}
public List<String> fetchEpisodeList(Map<String, ?> parameters) throws Exception {
Map<Option, Object> option = getDefaultOptions(parameters);
synchronized (cli) {
try {
return cli.fetchEpisodeList(asString(option.get(Option.query)), asString(option.get(Option.format)), asString(option.get(Option.db)), asString(option.get(Option.order)), asString(option.get(Option.lang)));
} catch (Exception e) {
printException(e);
return null;
}
}
}
public String getMediaInfo(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
synchronized (cli) {
try {
return cli.getMediaInfo(input.get(0), asString(option.get(Option.format)));
} catch (Exception e) {
printException(e);
return null;
}
}
}
private List<File> getInputFileList(Map<String, ?> map) {
Object file = map.get("file");
if (file != null) {
return FileUtilities.asFileList(file);
}
Object folder = map.get("folder");
if (folder != null) {
return FileUtilities.listFiles(FileUtilities.asFileList(folder), 0, false, true, false);
}
throw new IllegalArgumentException("file is not set");
}
private Map<Option, Object> getDefaultOptions(Map<String, ?> parameters) throws Exception {
Map<Option, Object> options = new EnumMap<Option, Object>(Option.class);
for (Entry<String, ?> it : parameters.entrySet()) {
try {
options.put(Option.valueOf(it.getKey()), it.getValue());
} catch (IllegalArgumentException e) {
// just ignore illegal options
}
}
ArgumentBean defaultValues = Settings.getApplicationArguments();
for (OptionName missing : EnumSet.complementOf(EnumSet.copyOf(options.keySet()))) {
if (missing == OptionName.strict) {
for (Option missing : complementOf(copyOf(options.keySet()))) {
switch (missing) {
case forceExtractAll:
options.put(missing, false);
break;
case strict:
options.put(missing, !defaultValues.nonStrict);
} else {
Object value = defaultValues.getClass().getField(missing.name()).get(defaultValues);
options.put(missing, value);
break;
default:
options.put(missing, defaultValues.getClass().getField(missing.name()).get(defaultValues));
break;
}
}
return options;
}
private RenameAction getRenameFunction(final Object obj) {
if (obj instanceof RenameAction) {
return (RenameAction) obj;
}
if (obj instanceof CharSequence) {
return StandardRenameAction.forName(obj.toString());
}
if (obj instanceof Closure<?>) {
return new RenameAction() {
private final Closure<?> closure = (Closure<?>) obj;
@Override
public File rename(File from, File to) throws Exception {
Object value = closure.call(from, to);
// must return File object, so we try the result of the closure, but if it's not a File we just return the original destination parameter
return value instanceof File ? (File) value : to;
}
@Override
public String toString() {
return "CLOSURE";
}
};
}
// object probably can't be casted
return (RenameAction) DefaultTypeTransformation.castToType(obj, RenameAction.class);
}
}

View File

@ -39,6 +39,14 @@ import com.cedarsoftware.util.io.JsonWriter;
public class ScriptShellMethods {
public static File plus(File self, String name) {
return new File(self.getPath().concat(name));
}
public static File div(File self, String name) {
return new File(self, name);
}
public static File resolve(File self, Object name) {
return new File(self, name.toString());
}
@ -252,6 +260,18 @@ public class ScriptShellMethods {
return WebRequest.post(self, text.getBytes("UTF-8"), "text/plain", requestParameters);
}
public static File saveAs(ByteBuffer self, String path) throws IOException {
return saveAs(self, new File(path));
}
public static File saveAs(String self, String path) throws IOException {
return saveAs(self, new File(path));
}
public static File saveAs(URL self, String path) throws IOException {
return saveAs(self, new File(path));
}
public static File saveAs(ByteBuffer self, File file) throws IOException {
// resolve relative paths
file = file.getAbsoluteFile();
@ -308,7 +328,7 @@ public class ScriptShellMethods {
return new NameSimilarityMetric().getSimilarity(self, other);
}
public static Collection<?> getSimilarity(Collection<?> self, final Object prime, final Closure<String> toStringFunction) {
public static Collection<?> sortBySimilarity(Collection<?> self, final Object prime, final Closure<String> toStringFunction) {
final SimilarityMetric metric = new NameSimilarityMetric();
List<Object> values = new ArrayList<Object>(self);
Collections.sort(values, new Comparator<Object>() {

View File

@ -1,269 +0,0 @@
import static net.sourceforge.filebot.util.FileUtilities.*
import java.util.regex.Pattern
/**
* Allow getAt() for File paths
*
* e.g. file[0] -> "F:"
*/
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) }
File.metaClass.getRoot = { listPath(delegate)[0] }
File.metaClass.listPath = { int tailSize = 255, boolean reversePath = false -> listPathTail(delegate, tailSize, reversePath) }
File.metaClass.getRelativePathTail = { int tailSize -> getRelativePathTail(delegate, tailSize) }
File.metaClass.getDiskSpace = { listPath(delegate).reverse().find{ it.exists() }?.usableSpace ?: 0 }
/**
* Convenience methods for String.toLowerCase() and String.toUpperCase()
*/
String.metaClass.lower = { toLowerCase() }
String.metaClass.upper = { toUpperCase() }
/**
* Allow comparison of Strings and Numbers (overloading of comparison operators is not supported yet though)
*/
String.metaClass.compareTo = { Number other -> delegate.compareTo(other.toString()) }
Number.metaClass.compareTo = { String other -> delegate.toString().compareTo(other) }
/**
* Pad strings or numbers with given characters ('0' by default).
*
* e.g. "1" -> "01"
*/
String.metaClass.pad = Number.metaClass.pad = { length = 2, padding = "0" -> delegate.toString().padLeft(length, padding) }
/**
* Return a substring matching the given pattern or break.
*/
String.metaClass.match = { String pattern, matchGroup = null ->
def matcher = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.MULTILINE | Pattern.DOTALL).matcher(delegate)
if (matcher.find())
return matcher.groupCount() > 0 && matchGroup == null ? matcher.group(1) : matcher.group(matchGroup ?: 0)
else
throw new Exception("Match failed")
}
/**
* Return a list of all matching patterns or break.
*/
String.metaClass.matchAll = { String pattern, int matchGroup = 0 ->
def matches = []
def matcher = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher(delegate)
while(matcher.find())
matches += matcher.group(matchGroup)
if (matches.size() > 0)
return matches
else
throw new Exception("Match failed")
}
/**
* Use empty string as default replacement.
*/
String.metaClass.replaceAll = { String pattern -> replaceAll(pattern, "") }
/**
* Replace space characters with a given characters.
*
* e.g. "Doctor Who" -> "Doctor_Who"
*/
String.metaClass.space = { replacement -> replaceAll(/[:?._]/, " ").trim().replaceAll(/\s+/, replacement) }
/**
* Upper-case all initials.
*
* e.g. "The Day a new Demon was born" -> "The Day A New Demon Was Born"
*/
String.metaClass.upperInitial = { replaceAll(/(?<=[&()+.,-;<=>?\[\]_{|}~ ]|^)[a-z]/, { it.toUpperCase() }) }
/**
* Get acronym, i.e. first letter of each word.
*
* e.g. "Deep Space 9" -> "DS9"
*/
String.metaClass.acronym = { delegate.sortName('$2').findAll(/(?<=[&()+.,-;<=>?\[\]_{|}~ ]|^)[\p{Alnum}]/).join().toUpperCase() }
String.metaClass.sortName = { replacement = '$2, $1' -> delegate.replaceFirst(/^(?i)(The|A|An)\s(.+)/, replacement).trim() }
/**
* Lower-case all letters that are not initials.
*
* e.g. "Gundam SEED" -> "Gundam Seed"
*/
String.metaClass.lowerTrail = { replaceAll(/\b(\p{Alpha})(\p{Alpha}+)\b/, { match, initial, trail -> initial + trail.toLowerCase() }) }
/**
* Return substring before the given pattern.
*/
String.metaClass.before = {
def matcher = delegate =~ it
// pattern was found, return leading substring, else return original value
return matcher.find() ? delegate.substring(0, matcher.start()) : delegate
}
/**
* Return substring after the given pattern.
*/
String.metaClass.after = {
def matcher = delegate =~ it
// pattern was found, return trailing substring, else return original value
return matcher.find() ? delegate.substring(matcher.end(), delegate.length()) : delegate
}
/**
* Replace trailing parenthesis including any leading whitespace.
*
* e.g. "The IT Crowd (UK)" -> "The IT Crowd"
*/
String.metaClass.replaceTrailingBrackets = { replacement = "" -> replaceAll(/\s*[(]([^)]*)[)]$/, replacement) }
/**
* Replace 'part identifier'.
*
* e.g. "Today Is the Day: Part 1" -> "Today Is the Day, Part 1"
* "Today Is the Day (1)" -> "Today Is the Day, Part 1"
*/
String.metaClass.replacePart = { replacement = "" ->
// handle '(n)', '(Part n)' and ': Part n' like syntax
for (pattern in [/\s*[(](\w+)[)]$/, /(?i)\W+Part (\w+)\W*$/]) {
if ((delegate =~ pattern).find()) {
return replaceAll(pattern, replacement);
}
}
// no pattern matches, nothing to replace
return delegate;
}
/**
* Apply ICU transliteration
* @see http://userguide.icu-project.org/transforms/general
*/
String.metaClass.transliterate = { transformIdentifier -> com.ibm.icu.text.Transliterator.getInstance(transformIdentifier).transform(delegate) }
/**
* Convert Unicode to ASCII as best as possible. Works with most alphabets/scripts used in the world.
*
* e.g. "Österreich" -> "Osterreich"
* "カタカナ" -> "katakana"
*/
String.metaClass.ascii = { fallback = ' ' -> delegate.transliterate("Any-Latin;Latin-ASCII;[:Diacritic:]remove").replaceAll("[^\\p{ASCII}]+", fallback) }
/**
* Replace multiple replacement pairs
*
* e.g. replace('ä', 'ae', 'ö', 'oe', 'ü', 'ue')
*/
String.metaClass.replace = { String... tr ->
String s = delegate;
for (int i = 0; i < tr.length-1; i+=2) {
CharSequence t = tr[i]
CharSequence r = tr[i+1]
s = s.replace(t, r)
}
return s
}
/**
* General helpers and utilities
*/
def c(Closure c) {
try {
return c.call()
} catch (Throwable e) {
return null
}
}
def any(Closure... closures) {
return closures.findResult{ c ->
try {
return c.call()
} catch (Throwable e) {
return null
}
}
}
def allOf(Closure... closures) {
return closures.toList().findResults{ c ->
try {
return c.call()
} catch (Throwable e) {
return null
}
}
}
def csv(path, delim = ';', keyIndex = 0, valueIndex = 1) {
def f = path as File
def values = [:]
if (f.isFile()) {
f.splitEachLine(delim, 'UTF-8') { line ->
values.put(line[keyIndex], c{ line[valueIndex] })
}
}
return values
}
Object.metaClass.match = { Map cases ->
def val = delegate;
cases.findResult {
switch(val) { case it.key: return it.value}
}
}
/**
* Web and File IO helpers
*/
import net.sourceforge.filebot.web.WebRequest
import net.sourceforge.filebot.util.FileUtilities
import net.sourceforge.filebot.util.XPathUtilities
URL.metaClass.getText = { FileUtilities.readAll(WebRequest.getReader(delegate.openConnection())) }
URL.metaClass.getHtml = { new XmlParser(new org.cyberneko.html.parsers.SAXParser()).parseText(delegate.getText()) }
URL.metaClass.getXml = { new XmlParser().parseText(delegate.getText()) }
URL.metaClass.scrape = { xpath -> XPathUtilities.selectString(xpath, WebRequest.getHtmlDocument(delegate)) }
URL.metaClass.scrapeAll = { xpath -> XPathUtilities.selectNodes(xpath, WebRequest.getHtmlDocument(delegate)).findResults{ XPathUtilities.getTextContent(it) } }
/**
* XML / XPath utility functions
*/
import javax.xml.xpath.XPathFactory
import javax.xml.xpath.XPathConstants
File.metaClass.xpath = URL.metaClass.xpath = { String xpath ->
def input = new org.xml.sax.InputSource(new StringReader(delegate.getText()))
def result = XPathFactory.newInstance().newXPath().evaluate(xpath, input, XPathConstants.STRING)
return result.trim();
}
File.metaClass.xpath = URL.metaClass.xpathAll = { String xpath ->
def input = new org.xml.sax.InputSource(new StringReader(delegate.getText()))
def nodes = XPathFactory.newInstance().newXPath().evaluate(xpath, input, XPathConstants.NODESET)
return [0..nodes.length-1].findResults{ i -> nodes.item(i).getTextContent().trim() }
}

View File

@ -2,8 +2,14 @@ package net.sourceforge.filebot.format;
import groovy.lang.Closure;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Global functions available in the {@link ExpressionFormat}
@ -82,4 +88,13 @@ public class ExpressionFormatFunctions {
return obj;
}
public Map<String, String> csv(String path) throws IOException {
Map<String, String> map = new LinkedHashMap<String, String>();
for (String line : Files.readAllLines(Paths.get(path), Charset.forName("UTF-8"))) {
String[] field = line.split(";", 2);
map.put(field[0], field[1]);
}
return map;
}
}

View File

@ -1,41 +0,0 @@
package net.sourceforge.filebot.format;
import groovy.lang.GroovyObjectSupport;
public class UndefinedObject extends GroovyObjectSupport {
private String value;
private UndefinedObject(String value) {
this.value = value;
}
@Override
public Object getProperty(String property) {
return this;
}
@Override
public Object invokeMethod(String name, Object args) {
return this;
}
@Override
public void setProperty(String property, Object newValue) {
// ignore
}
@Override
public String toString() {
return value;
}
}

View File

@ -559,7 +559,7 @@ public final class FileUtilities {
} else if (it instanceof Path) {
files.add(((Path) it).toFile());
} else if (it instanceof Collection<?>) {
files.addAll(asFileList(it)); // flatten object structure
files.addAll(asFileList(((Collection<?>) it).toArray())); // flatten object structure
}
}
return files;

View File

@ -1,29 +1,27 @@
package net.sourceforge.filebot.util;
import static java.util.Arrays.*;
import java.util.Iterator;
public final class StringUtilities {
public static String asString(Object object) {
return object == null ? null : object.toString();
}
public static boolean isEmptyValue(Object object) {
return object == null || object.toString().length() == 0;
}
public static String joinBy(CharSequence delimiter, Object... values) {
return join(asList(values), delimiter);
}
public static String join(Object[] values, CharSequence delimiter) {
return join(asList(values), delimiter);
}
public static String join(Iterable<?> values, CharSequence delimiter) {
StringBuilder sb = new StringBuilder();
@ -41,7 +39,6 @@ public final class StringUtilities {
return sb.toString();
}
/**
* Dummy constructor to prevent instantiation.
*/