diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 307d141f..4acd0ba6 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -7,7 +7,14 @@ import static net.sourceforge.tuned.ExceptionUtilities.*; import static net.sourceforge.tuned.FileUtilities.*; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.security.AccessController; import java.util.ArrayList; import java.util.Collection; @@ -25,6 +32,8 @@ import org.kohsuke.args4j.CmdLineParser; import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.MediaTypes; +import net.sourceforge.filebot.cli.ScriptShell.ScriptProvider; +import net.sourceforge.filebot.web.CachedResource; public class ArgumentProcessor { @@ -111,11 +120,11 @@ public class ArgumentProcessor { Bindings bindings = new SimpleBindings(); bindings.put("args", args.getFiles(false)); - ScriptShell shell = new ScriptShell(cli, args, args.parameters, args.trustScript, AccessController.getContext()); - URI script = shell.getScriptLocation(args.script); + ScriptProvider scriptProvider = new DefaultScriptProvider(); + Analytics.trackEvent("CLI", "ExecuteScript", scriptProvider.getScriptLocation(args.script).getScheme()); - Analytics.trackEvent("CLI", "ExecuteScript", script.getScheme()); - shell.run(script, bindings); + ScriptShell shell = new ScriptShell(cli, args, args.parameters, args.trustScript, AccessController.getContext(), scriptProvider); + shell.runScript(args.script, bindings); } CLILogger.finest("Done ヾ(@⌒ー⌒@)ノ"); @@ -133,4 +142,67 @@ public class ArgumentProcessor { System.out.println(" -X= : Define script variable"); } + + public static class DefaultScriptProvider implements ScriptProvider { + + @Override + public URI getScriptLocation(String input) { + try { + return new URL(input).toURI(); + } catch (Exception eu) { + try { + // fn:sortivo + if (input.startsWith("fn:")) { + return new URI("http", "filebot.sourceforge.net", "/scripts/" + input.substring(3) + ".groovy", null); + } + + // script:println 'hello world' + if (input.startsWith("script:")) { + return new URI("script", input.substring(7), null, null, null); + } + + // system:in + if (input.equals("system:in")) { + return new URI("system", "in", null, null, null); + } + + // X:/foo/bar.groovy + File file = new File(input); + if (!file.isFile()) { + throw new FileNotFoundException(file.getPath()); + } + return file.toURI(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + } + + + @Override + public String fetchScript(URI uri) throws IOException { + if (uri.getScheme().equals("file")) { + return readAll(new InputStreamReader(new FileInputStream(new File(uri)), "UTF-8")); + } + + if (uri.getScheme().equals("system")) { + return readAll(new InputStreamReader(System.in)); + } + + if (uri.getScheme().equals("script")) { + return uri.getAuthority(); + } + + // fetch remote script only if modified + CachedResource script = new CachedResource(uri.toString(), String.class, 24 * 60 * 60 * 1000) { + + @Override + public String process(ByteBuffer data) { + return Charset.forName("UTF-8").decode(data).toString(); + } + }; + return script.get(); + } + } + } diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index 4b695a4f..3df5d087 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -498,7 +498,7 @@ public class CmdlineOperations implements CmdlineInterface { HistorySpooler.getInstance().append(renameMap.entrySet()); // printer number of renamed files if any - CLILogger.fine(format("Renamed %d files", renameLog.size())); + CLILogger.fine(format("Processed %d files", renameLog.size())); } } diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.java b/source/net/sourceforge/filebot/cli/ScriptShell.java index 61677334..d48c21fd 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.java +++ b/source/net/sourceforge/filebot/cli/ScriptShell.java @@ -3,23 +3,14 @@ package net.sourceforge.filebot.cli; import static net.sourceforge.filebot.cli.CLILogging.*; -import static net.sourceforge.tuned.FileUtilities.*; import java.awt.AWTPermission; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FilePermission; import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; import java.lang.reflect.ReflectPermission; import java.net.SocketPermission; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.security.AccessControlContext; import java.security.AccessController; import java.security.Permissions; @@ -45,7 +36,6 @@ import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.format.AssociativeScriptObject; import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.PrivilegedInvocation; -import net.sourceforge.filebot.web.CachedResource; import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.MovieIdentificationService; @@ -55,8 +45,11 @@ class ScriptShell { private final ScriptEngine engine = new GroovyScriptEngineFactory().getScriptEngine(); private final boolean trustScript; + private final ScriptProvider scriptProvider; - public ScriptShell(CmdlineInterface cli, ArgumentBean args, Map parameters, boolean trustScript, AccessControlContext acc) throws ScriptException { + + public ScriptShell(CmdlineInterface cli, ArgumentBean args, Map parameters, boolean trustScript, AccessControlContext acc, ScriptProvider scriptProvider) throws ScriptException { + this.scriptProvider = scriptProvider; this.trustScript = trustScript; // setup script context @@ -70,100 +63,19 @@ class ScriptShell { } - protected Bindings initializeBindings(CmdlineInterface cli, ArgumentBean args, Map parameters, AccessControlContext acc) { - Bindings bindings = new SimpleBindings(); + public static interface ScriptProvider { - // bind external parameters - for (Entry it : parameters.entrySet()) { - bindings.put(it.getKey(), it.getValue()); - } + public URI getScriptLocation(String input); - // bind API objects - bindings.put("_cli", PrivilegedInvocation.newProxy(CmdlineInterface.class, cli, acc)); - bindings.put("_script", new File(args.script)); - bindings.put("_args", args); - bindings.put("_types", MediaTypes.getDefault()); - bindings.put("_log", CLILogger); - - // bind Java properties and environment variables - bindings.put("_parameter", new AssociativeScriptObject(parameters)); - bindings.put("_system", new AssociativeScriptObject(System.getProperties())); - bindings.put("_environment", new AssociativeScriptObject(System.getenv())); - - // bind console object - bindings.put("console", System.console()); - - // bind Episode data providers - for (EpisodeListProvider service : WebServices.getEpisodeListProviders()) { - bindings.put(service.getName(), service); - } - - // bind Movie data providers - for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) { - bindings.put(service.getName(), service); - } - - return bindings; + public String fetchScript(URI uri) throws Exception; } - public URI getScriptLocation(String input) { - try { - return new URL(input).toURI(); - } catch (Exception eu) { - if (input.startsWith("script:")) { - try { - return new URI("script", input.substring(7), null, null, null); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - } - try { - File file = new File(input); - if (!file.exists()) { - throw new FileNotFoundException(file.getPath()); - } - return file.toURI(); - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - } - } - - - public Object run(String input, Bindings bindings) throws Throwable { - return run(getScriptLocation(input), bindings); - } - - - public Object run(URI scriptLocation, Bindings bindings) throws Throwable { - if (scriptLocation.getScheme().equals("file")) { - return evalute(new InputStreamReader(new FileInputStream(new File(scriptLocation)), "UTF-8"), bindings); - } - - if (scriptLocation.getScheme().equals("system")) { - return evalute(new InputStreamReader(System.in), bindings); - } - - if (scriptLocation.getScheme().equals("script")) { - return evalute(new StringReader(scriptLocation.getAuthority()), bindings); - } - - // fetch remote script only if modified - CachedResource script = new CachedResource(scriptLocation.toString(), String.class, 0) { - - @Override - public String process(ByteBuffer data) { - return Charset.forName("UTF-8").decode(data).toString(); - } - }; - return evaluate(script.get(), bindings); - } - - - public Object evalute(Reader script, Bindings bindings) throws Throwable { - return evaluate(readAll(script), bindings); + public Object runScript(String input, Bindings bindings) throws Throwable { + URI resource = scriptProvider.getScriptLocation(input); + String script = scriptProvider.fetchScript(resource); + return evaluate(script, bindings); } @@ -190,6 +102,45 @@ class ScriptShell { } + protected Bindings initializeBindings(CmdlineInterface cli, ArgumentBean args, Map parameters, AccessControlContext acc) { + Bindings bindings = new SimpleBindings(); + + // bind external parameters + for (Entry it : parameters.entrySet()) { + bindings.put(it.getKey(), it.getValue()); + } + + // bind API objects + bindings.put("_cli", PrivilegedInvocation.newProxy(CmdlineInterface.class, cli, acc)); + bindings.put("_script", new File(args.script)); + bindings.put("_args", args); + bindings.put("_shell", this); + + bindings.put("_types", MediaTypes.getDefault()); + bindings.put("_log", CLILogger); + + // bind Java properties and environment variables + bindings.put("_parameter", new AssociativeScriptObject(parameters)); + bindings.put("_system", new AssociativeScriptObject(System.getProperties())); + bindings.put("_environment", new AssociativeScriptObject(System.getenv())); + + // bind console object + bindings.put("console", System.console()); + + // bind Episode data providers + for (EpisodeListProvider service : WebServices.getEpisodeListProviders()) { + bindings.put(service.getName(), service); + } + + // bind Movie data providers + for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) { + bindings.put(service.getName(), service); + } + + return bindings; + } + + protected AccessControlContext getSandboxAccessControlContext() { Permissions permissions = new Permissions(); diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index 6a0215b0..d5e4e8ea 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -182,6 +182,14 @@ List.metaClass.sortBySimilarity = { prime, Closure toStringFunction = { obj -> o return delegate.sort{ a, b -> similarity(toStringFunction(b), prime).compareTo(similarity(toStringFunction(a), prime)) } } +// call scripts +def include(String input, Map parameters = [:], Object... args) { + // initialize default parameter + parameters.args = (args as List).findResults{ it as File } + + // run given script and catch exceptions + _guarded { _shell.runScript(input, new javax.script.SimpleBindings(parameters)) } +} // CLI bindings diff --git a/source/net/sourceforge/filebot/web/CachedResource.java b/source/net/sourceforge/filebot/web/CachedResource.java index 5212d610..e8674f59 100644 --- a/source/net/sourceforge/filebot/web/CachedResource.java +++ b/source/net/sourceforge/filebot/web/CachedResource.java @@ -76,6 +76,12 @@ public abstract class CachedResource { } catch (Exception e) { throw new IOException(e); } + } else { + try { + product = type.cast(element.getValue()); + } catch (Exception e) { + Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage()); + } } try { diff --git a/website/scripts/lib/xbmc.groovy b/website/scripts/lib/xbmc.groovy new file mode 100644 index 00000000..745f2c77 --- /dev/null +++ b/website/scripts/lib/xbmc.groovy @@ -0,0 +1,136 @@ +// xbmc functions +def invokeScanVideoLibrary(host, port = 9090) { + try { + telnet(host, 9090) { writer, reader -> + writer.println('{"id":1,"method":"VideoLibrary.Scan","params":[],"jsonrpc":"2.0"}') // API call for latest XBMC release + } + return true + } catch(e) { + println "${e.class.simpleName}: ${e.message}" + return false + } +} + + +// functions for TheTVDB artwork/nfo +def fetchSeriesBanner(outputFile, series, bannerType, bannerType2, season, locale) { + // select and fetch banner + def banner = [locale, null].findResult { TheTVDB.getBanner(series, [BannerType:bannerType, BannerType2:bannerType2, Season:season, Language:it]) } + if (banner == null) { + println "Banner not found: $outputFile / $bannerType:$bannerType2" + return null + } + println "Fetching $outputFile => $banner" + return banner.url.saveAs(outputFile) +} + +def fetchSeriesNfo(outputFile, series, locale) { + def info = TheTVDB.getSeriesInfo(series, locale) + info.applyXmlTemplate(''' + $name + $firstAired.year + + -1 + + + -1 + -1 + $rating + $ratingCount + + $overview + + $runtime + $contentRating + + + $id + http://www.thetvdb.com/api/1D62F2F90030C444/series/${id}/all/''' + locale.language + '''.zip + ${!genres.empty ? genres[0] : ''} + + + + $bannerUrl + $firstAired + $status + $network + + actors.each { + + $it + + + } + + + ''') + .replaceAll(/\t|\r|\n/, '') // xbmc can't handle leading/trailing whitespace properly + .saveAs(outputFile) +} + +def fetchSeriesArtworkAndNfo(seriesDir, seasonDir, series, season, locale = _args.locale) { + try { + // fetch nfo + fetchSeriesNfo(seriesDir['tvshow.nfo'], series, locale) + + // fetch series banner, fanart, posters, etc + ["680x1000", null].findResult{ fetchSeriesBanner(seriesDir['folder.jpg'], series, "poster", it, null, locale) } + ["graphical", null].findResult{ fetchSeriesBanner(seriesDir['banner.jpg'], series, "series", it, null, locale) } + + // fetch highest resolution fanart + ["1920x1080", "1280x720", null].findResult{ fetchSeriesBanner(seriesDir["fanart.jpg"], series, "fanart", it, null, locale) } + + // fetch season banners + if (seasonDir != seriesDir) { + fetchSeriesBanner(seasonDir["folder.jpg"], series, "season", "season", season, locale) + fetchSeriesBanner(seasonDir["banner.jpg"], series, "season", "seasonwide", season, locale) + } + } catch(e) { + println "${e.class.simpleName}: ${e.message}" + } +} + + + +// functions for TheMovieDB artwork/nfo +def fetchMovieArtwork(outputFile, movieInfo, artworkType, artworkSize) { + // select and fetch artwork + def artwork = movieInfo.images.find { it.type == artworkType && it.size == artworkSize } + if (artwork == null) { + println "Artwork not found: $outputFile" + return null + } + println "Fetching $outputFile => $artwork" + return artwork.url.saveAs(outputFile) +} + +def fetchMovieNfo(outputFile, movieInfo) { + movieInfo.applyXmlTemplate(''' + $name + $released.year + $rating + $votes + $overview + $runtime + $certification + ${!genres.empty ? genres[0] : ''} + tt${imdbId.pad(7)} + + ''') + .replaceAll(/\t|\r|\n/, '') // xbmc can't handle leading/trailing whitespace properly + .saveAs(outputFile) +} + +def fetchMovieArtworkAndNfo(movieDir, movie, locale = _args.locale) { + try { + def movieInfo = TheMovieDB.getMovieInfo(movie, locale) + // fetch nfo + fetchMovieNfo(movieDir['movie.nfo'], movieInfo) + + // fetch series banner, fanart, posters, etc + fetchMovieArtwork(movieDir['folder.jpg'], movieInfo, 'poster', 'original') + fetchMovieArtwork(movieDir['backdrop.jpg'], movieInfo, 'backdrop', 'original') + } catch(e) { + println "${e.class.simpleName}: ${e.message}" + } +} diff --git a/website/scripts/utorrent-postprocess.groovy b/website/scripts/utorrent-postprocess.groovy index e184d8d7..80491eef 100644 --- a/website/scripts/utorrent-postprocess.groovy +++ b/website/scripts/utorrent-postprocess.groovy @@ -1,4 +1,4 @@ -// filebot -script "http://filebot.sf.net/scripts/utorrent-postprocess.groovy" --output "X:/media" --action copy --conflict override -non-strict -trust-script -Xxbmc=localhost "-Xut_dir=%D" "-Xut_file=%F" "-Xut_label=%L" "-Xut_state=%S" "-Xut_kind=%K" +// filebot -script "fn:utorrent-postprocess" --output "X:/media" --action copy --conflict override -non-strict -trust-script -Xxbmc=localhost "-Xut_dir=%D" "-Xut_file=%F" "-Xut_label=%L" "-Xut_state=%S" "-Xut_kind=%K" def input = [] // print input parameters @@ -19,6 +19,9 @@ input = input.findAll{ it.isVideo() || it.isSubtitle() } // print input fileset input.each{ println "Input: $it" } +// xbmc artwork/nfo utility +include("fn:lib/xbmc") + // group episodes/movies and rename according to XBMC standards def groups = input.groupBy{ def tvs = detectSeriesName(it) @@ -39,38 +42,35 @@ def groups = input.groupBy{ } groups.each{ group, files -> + // fetch subtitles + def subs = getMissingSubtitles(file:files, output:"srt", encoding:"utf-8") + if (subs) files += subs + // EPISODE MODE if (group.tvs && !group.mov) { - def output = rename(file:files, format:'TV Shows/{n}{episode.special ? "/Special" : "/Season "+s}/{n} - {episode.special ? "S00E"+special.pad(2) : s00e00} - {t}', db:'TheTVDB') + def dest = rename(file:files, format:'TV Shows/{n}/{episode.special ? "Special" : "Season "+s}/{n} - {episode.special ? "S00E"+special.pad(2) : s00e00} - {t}', db:'TheTVDB') - output*.dir.unique().each{ dir -> + dest.mapByFolder().keySet().each{ dir -> println "Fetching artwork for $dir from TheTVDB" def query = group.tvs - def sxe = output.findResult{ parseEpisodeNumber(it) } - def options = TheTVDB.search(query, _args.locale) + def sxe = dest.findResult{ parseEpisodeNumber(it) } + def options = TheTVDB.search(query) if (options.isEmpty()) { println "TV Series not found: $query" return } options = options.sortBySimilarity(query, { it.name }) - def series = options[0] - def seriesDir = [dir.dir, dir].sortBySimilarity(series.name, { it.name })[0] - def season = sxe && sxe.season > 0 ? sxe.season : 1 - fetchSeriesBannersAndNfo(seriesDir, dir, series, season) + fetchSeriesArtworkAndNfo(dir.dir, dir, options[0], sxe && sxe.season > 0 ? sxe.season : 1) } } // MOVIE MODE if (group.mov && !group.tvs) { - def output = rename(file:files, format:'Movies/{n} ({y})/{n} ({y}){" CD$pi"}', db:'TheMovieDB') + def dest = rename(file:files, format:'Movies/{n} ({y})/{n} ({y}){" CD$pi"}', db:'TheMovieDB') - output*.dir.unique().each{ dir -> + dest.mapByFolder().keySet().each{ dir -> println "Fetching artwork for $dir from TheMovieDB" - try { - fetchMovieArtworkAndNfo(dir, group.mov) - } catch(e) { - println "${e.class.simpleName}: ${e.message}" - } + fetchMovieArtworkAndNfo(dir, group.mov) } } } @@ -78,124 +78,7 @@ groups.each{ group, files -> // make XBMC scan for new content -try { - xbmc.split(/[\s,|]+/).each{ - println "Notify XBMC: $it" - telnet(it, 9090) { writer, reader -> - def msg = '{"id":1,"method":"VideoLibrary.Scan","params":[],"jsonrpc":"2.0"}' - writer.println(msg) - } - } -} catch(e) { - println "${e.class.simpleName}: ${e.message}" -} -// END OF SCRIPT - - - - -// FUNCTIONS for TMDB and TVDB artwork/nfo -def fetchBanner(outputFile, series, bannerType, bannerType2 = null, season = null) { - // select and fetch banner - def banner = [_args.locale.language, null].findResult { TheTVDB.getBanner(series, [BannerType:bannerType, BannerType2:bannerType2, Season:season, Language:it]) } - if (banner == null) { - println "Banner not found: $outputFile / $bannerType:$bannerType2" - return null - } - println "Fetching $outputFile => $banner" - return banner.url.saveAs(outputFile) -} -def fetchSeriesNfo(outputFile, series) { - def info = TheTVDB.getSeriesInfo(series, _args.locale) - info.applyXmlTemplate(''' - $name - $firstAired.year - - -1 - - - -1 - -1 - $rating - $ratingCount - - $overview - - $runtime - $contentRating - - - $id - http://www.thetvdb.com/api/1D62F2F90030C444/series/${id}/all/''' + _args.locale.language + '''.zip - ${!genres.empty ? genres[0] : ''} - - - - $bannerUrl - $firstAired - $status - $network - - actors.each { - - $it - - - } - - - ''') - .replaceAll(/\t|\r|\n/, '') // xbmc can't handle leading/trailing whitespace properly - .saveAs(outputFile) -} -def fetchSeriesBannersAndNfo(seriesDir, seasonDir, series, season) { - println "Fetch nfo and banners for $series / Season $season" - // fetch nfo - fetchSeriesNfo(seriesDir['tvshow.nfo'], series) - // fetch series banner, fanart, posters, etc - ["680x1000", null].findResult{ fetchBanner(seriesDir['folder.jpg'], series, "poster", it) } - ["graphical", null].findResult{ fetchBanner(seriesDir['banner.jpg'], series, "series", it) } - // fetch highest resolution fanart - ["1920x1080", "1280x720", null].findResult{ fetchBanner(seriesDir["fanart.jpg"], series, "fanart", it) } - // fetch season banners - if (seasonDir != seriesDir) { - fetchBanner(seasonDir["folder.jpg"], series, "season", "season", season) - fetchBanner(seasonDir["banner.jpg"], series, "season", "seasonwide", season) - } -} - -def fetchArtwork(outputFile, movieInfo, artworkType, artworkSize) { - // select and fetch artwork - def artwork = movieInfo.images.find { it.type == artworkType && it.size == artworkSize } - if (artwork == null) { - println "Artwork not found: $outputFile" - return null - } - println "Fetching $outputFile => $artwork" - return artwork.url.saveAs(outputFile) -} -def fetchMovieNfo(outputFile, movieInfo) { - movieInfo.applyXmlTemplate(''' - $name - $released.year - $rating - $votes - $overview - $runtime - $certification - ${!genres.empty ? genres[0] : ''} - tt${imdbId.pad(7)} - - ''') - .replaceAll(/\t|\r|\n/, '') // xbmc can't handle leading/trailing whitespace properly - .saveAs(outputFile) -} -def fetchMovieArtworkAndNfo(movieDir, movie) { - println "Fetch nfo and artwork for $movie" - def movieInfo = TheMovieDB.getMovieInfo(movie, Locale.ENGLISH) - // fetch nfo - fetchMovieNfo(movieDir['movie.nfo'], movieInfo) - // fetch series banner, fanart, posters, etc - fetchArtwork(movieDir['folder.jpg'], movieInfo, 'poster', 'original') - fetchArtwork(movieDir['backdrop.jpg'], movieInfo, 'backdrop', 'original') +xbmc.split(/[\s,|]+/).each{ + println "Notify XMBC: $it" + invokeScanVideoLibrary(it) }