+ Groovy engine extensions rewrite complete :)
This commit is contained in:
parent
2ba959e2b5
commit
ca3fc8f3fa
|
@ -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(/['`´‘’ʻ]+/, /'/)
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,46 +1,43 @@
|
|||
|
||||
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();
|
||||
|
||||
|
||||
for (Iterator<?> iterator = values.iterator(); iterator.hasNext();) {
|
||||
Object value = iterator.next();
|
||||
if (!isEmptyValue(value)) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(delimiter);
|
||||
}
|
||||
|
||||
|
||||
sb.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dummy constructor to prevent instantiation.
|
||||
|
@ -48,5 +45,5 @@ public final class StringUtilities {
|
|||
private StringUtilities() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue