diff --git a/BuildData.groovy b/BuildData.groovy index e78e02af..761895c6 100644 --- a/BuildData.groovy +++ b/BuildData.groovy @@ -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(/['`´‘’ʻ]+/, /'/) diff --git a/installer/nsis/filebot.nsi b/installer/nsis/filebot.nsi index b794fab8..24698c9c 100644 --- a/installer/nsis/filebot.nsi +++ b/installer/nsis/filebot.nsi @@ -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." diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.java b/source/net/sourceforge/filebot/cli/ScriptShell.java index 81831248..5178d9ae 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.java +++ b/source/net/sourceforge/filebot/cli/ScriptShell.java @@ -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() { diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy deleted file mode 100644 index 4598339a..00000000 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ /dev/null @@ -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) - } - } -} diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.properties b/source/net/sourceforge/filebot/cli/ScriptShell.properties index 749e4fe4..cc47b0cf 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.properties +++ b/source/net/sourceforge/filebot/cli/ScriptShell.properties @@ -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 \ No newline at end of file diff --git a/source/net/sourceforge/filebot/cli/ScriptShellBaseClass.java b/source/net/sourceforge/filebot/cli/ScriptShellBaseClass.java index 61604ca9..f536e2fd 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShellBaseClass.java +++ b/source/net/sourceforge/filebot/cli/ScriptShellBaseClass.java @@ -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 defaultValues; + private Map defaultValues; public void setDefaultValues(Map values) { - this.defaultValues = values; + this.defaultValues = new LinkedHashMap(values); } - public Map getDefaultValues() { + public Map 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 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 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 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 withDefaultOptions(Map map) throws Exception { - Map options = new EnumMap(OptionName.class); + private enum Option { + action, conflict, query, filter, format, db, order, lang, output, encoding, strict, forceExtractAll + } - for (Entry it : map.entrySet()) { - options.put(OptionName.valueOf(it.getKey()), it.getValue()); + private static final CmdlineInterface cli = new CmdlineOperations(); + + public List rename(Map parameters) throws Exception { + List input = getInputFileList(parameters); + Map 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 getSubtitles(Map parameters) throws Exception { + List input = getInputFileList(parameters); + Map 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 getMissingSubtitles(Map parameters) throws Exception { + List input = getInputFileList(parameters); + Map 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 parameters) throws Exception { + List input = getInputFileList(parameters); + + synchronized (cli) { + try { + return cli.check(input); + } catch (Exception e) { + printException(e); + return false; + } + } + } + + public File compute(Map parameters) throws Exception { + List input = getInputFileList(parameters); + Map 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 extract(Map parameters) throws Exception { + List input = getInputFileList(parameters); + Map 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 fetchEpisodeList(Map parameters) throws Exception { + Map 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 parameters) throws Exception { + List input = getInputFileList(parameters); + Map 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 getInputFileList(Map 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 getDefaultOptions(Map parameters) throws Exception { + Map options = new EnumMap(Option.class); + + for (Entry 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); + } + } diff --git a/source/net/sourceforge/filebot/cli/ScriptShellMethods.java b/source/net/sourceforge/filebot/cli/ScriptShellMethods.java index 3d7d9e6f..2aa92d07 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShellMethods.java +++ b/source/net/sourceforge/filebot/cli/ScriptShellMethods.java @@ -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 toStringFunction) { + public static Collection sortBySimilarity(Collection self, final Object prime, final Closure toStringFunction) { final SimilarityMetric metric = new NameSimilarityMetric(); List values = new ArrayList(self); Collections.sort(values, new Comparator() { diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy b/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy deleted file mode 100644 index ef5f523e..00000000 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.lib.groovy +++ /dev/null @@ -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() } -} diff --git a/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java b/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java index f3f6ae38..dd310500 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java +++ b/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java @@ -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 csv(String path) throws IOException { + Map map = new LinkedHashMap(); + 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; + } + } diff --git a/source/net/sourceforge/filebot/format/UndefinedObject.java b/source/net/sourceforge/filebot/format/UndefinedObject.java deleted file mode 100644 index b4360c4e..00000000 --- a/source/net/sourceforge/filebot/format/UndefinedObject.java +++ /dev/null @@ -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; - } - -} diff --git a/source/net/sourceforge/filebot/util/FileUtilities.java b/source/net/sourceforge/filebot/util/FileUtilities.java index 8e4b54aa..7a2d3281 100644 --- a/source/net/sourceforge/filebot/util/FileUtilities.java +++ b/source/net/sourceforge/filebot/util/FileUtilities.java @@ -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; diff --git a/source/net/sourceforge/filebot/util/StringUtilities.java b/source/net/sourceforge/filebot/util/StringUtilities.java index 694cccac..46a833c8 100644 --- a/source/net/sourceforge/filebot/util/StringUtilities.java +++ b/source/net/sourceforge/filebot/util/StringUtilities.java @@ -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(); } - + }