From 0cb56f905d84a1cb8292d89a02e6668e50a0ad66 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Mon, 30 Jul 2012 16:59:09 +0000 Subject: [PATCH] * utorrent integration: + fancy notification mails + force movie/series/anime + basic anime support (no auto-detection, only if forced) --- .../filebot/cli/ScriptShell.lib.groovy | 17 ++- website/scripts/lib/ant.groovy | 8 +- website/scripts/lib/htpc.groovy | 4 +- website/scripts/utorrent-postprocess.groovy | 100 +++++++++++++++--- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index 1d3d2e62..673c0a98 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -95,8 +95,19 @@ import groovy.text.GStringTemplateEngine import net.sourceforge.filebot.format.PropertyBindings import net.sourceforge.filebot.format.UndefinedObject -Object.metaClass.applyXmlTemplate = { template -> new XmlTemplateEngine("\t", false).createTemplate(template).make(new PropertyBindings(delegate, new UndefinedObject(""))).toString() } -Object.metaClass.applyTextTemplate = { template -> new GStringTemplateEngine().createTemplate(template).make(new PropertyBindings(delegate, new UndefinedObject(""))).toString() } +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) + bc.rehydrate(bc.delegate, xmb, xmb).call() // call closure in MarkupBuilder context + return out.toString() +} // Shell helper @@ -156,7 +167,7 @@ List.metaClass.watch = { c -> createWatchService(c, delegate, true) } 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.dir, it.to)] } + 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 diff --git a/website/scripts/lib/ant.groovy b/website/scripts/lib/ant.groovy index 78be7f4a..12850162 100644 --- a/website/scripts/lib/ant.groovy +++ b/website/scripts/lib/ant.groovy @@ -18,7 +18,7 @@ def sshexec(param) { * e.g. * mail(mailhost:'smtp.gmail.com', mailport:'587', ssl:'no', enableStartTLS:'yes', user:'rednoah@gmail.com', password:'correcthorsebatterystaple', from:'rednoah@gmail.com', to:'someone@gmail.com', subject:'Hello Ant World', message:'Dear Ant, ...') */ -def mail(param) { +def sendmail(param) { def sender = param.remove('from') def recipient = param.remove('to') @@ -35,12 +35,12 @@ def mail(param) { * e.g. * gmail(subject:'Hello Ant World', message:'Dear Ant, ...', to:'someone@gmail.com', user:'rednoah', password:'correcthorsebatterystaple') */ -def gmail(param) { +def sendGmail(param) { param << [mailhost:'smtp.gmail.com', mailport:'587', ssl:'no', enableStartTLS:'yes'] - param << [user:param.username ? param.remove('username') + "@gmail.com" : param.user] + param << [user:param.username ? param.remove('username') + '@gmail.com' : param.user] param << [from: param.from ?: param.user] - mail(param) + sendmail(param) } diff --git a/website/scripts/lib/htpc.groovy b/website/scripts/lib/htpc.groovy index 91bd394b..f1b491f4 100644 --- a/website/scripts/lib/htpc.groovy +++ b/website/scripts/lib/htpc.groovy @@ -66,7 +66,7 @@ def fetchSeriesFanart(outputFile, series, type, season, locale) { def fetchSeriesNfo(outputFile, series, locale) { def info = TheTVDB.getSeriesInfo(series, locale) - info.applyXmlTemplate(''' + info.applyXml(''' $name $firstAired.year $rating @@ -188,7 +188,7 @@ def createFileInfoXml(file) { } def fetchMovieNfo(outputFile, movieInfo, movieFile) { - movieInfo.applyXmlTemplate(''' + movieInfo.applyXml(''' $name $originalName $collection diff --git a/website/scripts/utorrent-postprocess.groovy b/website/scripts/utorrent-postprocess.groovy index f4d7f20e..1720b1d1 100644 --- a/website/scripts/utorrent-postprocess.groovy +++ b/website/scripts/utorrent-postprocess.groovy @@ -1,9 +1,9 @@ -// filebot -script "fn:utorrent-postprocess" --output "X:/media" --action copy --conflict override --def subtitles=true artwork=true xbmc=localhost plex=10.0.0.3 "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_label=%L" "ut_state=%S" +// filebot -script "fn:utorrent-postprocess" --output "X:/media" --action copy --conflict override --def subtitles=true artwork=true xbmc=localhost plex=10.0.0.1 gmail=username:password "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_title=%N" "ut_label=%L" "ut_state=%S" def input = [] def failOnError = _args.conflict == 'fail' // print input parameters -_args.bindings?.each{ println "Parameter: $it.key = $it.value" } +_args.bindings?.each{ _log.finest("Parameter: $it.key = $it.value") } // disable enable features as specified via --def parameters def subtitles = tryQuietly{ subtitles.toBoolean() } @@ -13,11 +13,31 @@ def artwork = tryQuietly{ artwork.toBoolean() } def xbmc = tryQuietly{ xbmc.split(/[\s,|]+/) } def plex = tryQuietly{ plex.split(/[\s,|]+/) } +// email notifications +def gmail = tryQuietly{ gmail.split(':', 2) } + +// force movie/series/anime logic +def forceMovie(f) { + tryQuietly{ ut_label } =~ /^(?i:Movie|Couch.Potato)/ +} + +def forceSeries(f) { + parseEpisodeNumber(f) || parseDate(f) || tryQuietly{ ut_label } =~ /^(?i:TV)/ +} + +def forceAnime(f) { + tryQuietly{ ut_label } =~ /^(?i:Anime)/ +} + +def forceIgnore(f) { + tryQuietly{ ut_label } =~ /^(?i:Music|Ebook|other)/ +} + // collect input fileset as specified by the given --def parameters if (args.empty) { // assume we're called with utorrent parameters - if (ut_kind == "single") { + if (ut_kind == 'single') { input += new File(ut_dir, ut_file) // single-file torrent } else { input += new File(ut_dir).getFiles() // multi-file torrent @@ -37,15 +57,26 @@ input = input.findAll{ it.isVideo() || it.isSubtitle() } input = input.findAll{ !(it.path =~ /\b(?i:sample|trailer|extras|deleted.scenes|music.video|scrapbook)\b/) } // print input fileset -input.each{ println "Input: $it" } +input.each{ f -> _log.finest("Input: $f") } // artwork/nfo utility include("lib/htpc") // group episodes/movies and rename according to XBMC standards def groups = input.groupBy{ f -> + // skip auto-detection if possible + if (forceIgnore(f)) + return [] + if (forceMovie(f)) + return [mov: detectMovie(f, false)] + if (forceSeries(f)) + return [tvs: detectSeriesName(f)] + if (forceAnime(f)) + return [anime: detectSeriesName(f)] + + def tvs = detectSeriesName(f) - def mov = (parseEpisodeNumber(f) || parseDate(f)) ? null : detectMovie(f, false) // skip movie detection if we can already tell it's an episode + def mov = detectMovie(f, false) println "$f.name [series: $tvs, movie: $mov]" // DECIDE EPISODE VS MOVIE (IF NOT CLEAR) @@ -71,9 +102,13 @@ def groups = input.groupBy{ f -> throw new Exception("Media detection failed") } - return [tvs:tvs, mov:mov] + return [tvs: tvs, mov: mov, anime: null] } +// log movie/series/anime detection results +groups.each{ group -> _log.finest("Group: $group") } + +// process each batch groups.each{ group, files -> // fetch subtitles if (subtitles) { @@ -81,28 +116,31 @@ groups.each{ group, files -> } // EPISODE MODE - if (group.tvs && !group.mov) { - 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') + if ((group.tvs || group.anime) && !group.mov) { + // choose series / anime config + def config = group.tvs ? [name: group.tvs, format:'TV Shows/{n}/{episode.special ? "Special" : "Season "+s}/{n} - {episode.special ? "S00E"+special.pad(2) : s00e00} - {t}', db:'TheTVDB'] + : [name: group.anime, format:'Anime/{n}/{n} - {e.pad(2)} - {t}', db:'AniDB'] + def dest = rename(file: files, format: config.format, db: config.db) if (dest && artwork) { dest.mapByFolder().each{ dir, fs -> println "Fetching artwork for $dir from TheTVDB" def sxe = fs.findResult{ eps -> parseEpisodeNumber(eps) } - def options = TheTVDB.search(group.tvs) + def options = TheTVDB.search(config.name) if (options.isEmpty()) { - println "TV Series not found: $group.tvs" + println "TV Series not found: $config.name" return } - options = options.sortBySimilarity(group.tvs, { opt -> opt.name }) + options = options.sortBySimilarity(config.name, { s -> s.name }) fetchSeriesArtworkAndNfo(dir.dir, dir, options[0], sxe && sxe.season > 0 ? sxe.season : 1) } } if (dest == null && failOnError) { - throw new Exception("Failed to rename series: $group.tvs") + throw new Exception("Failed to rename series: $config.name") } } // MOVIE MODE - if (group.mov && !group.tvs) { + if (group.mov && !group.tvs && !group.anime) { def dest = rename(file:files, format:'Movies/{n} ({y})/{n} ({y}){" CD$pi"}{".$lang"}', db:'TheMovieDB') if (dest && artwork) { dest.mapByFolder().each{ dir, fs -> @@ -128,3 +166,39 @@ plex?.each{ println "Notify Plex: $it" refreshPlexLibrary(it) } + +// send status email +if (gmail && getRenameLog().size() > 0) { + // ant/mail utility + include('lib/ant') + + // send html mail + def renameLog = getRenameLog() + + sendGmail( + subject: '[FileBot] ' + ut_title, + message: XML { + html { + body { + p("FileBot finished processing ${ut_title} (${renameLog.size()} files)."); + hr(); table { + th("Parameter"); th("Value") + _args.bindings.findAll{ param -> param.key =~ /^ut_/ }.each{ param -> + tr { [param.key, param.value].each{ td(it)} } + } + } + hr(); table { + th("Original Name"); th("New Name"); th("New Location") + renameLog.each{ from, to -> + tr { [from.name, to.name, to.parent].each{ cell -> td { code(cell) } } } + } + } + hr(); small("// Generated by ${net.sourceforge.filebot.Settings.applicationIdentifier} on ${new Date().dateString} at ${new Date().timeString}") + } + } + }, + messagemimetype: "text/html", + to: tryQuietly{ gmail2 } ?: gmail[0] =~ /@/ ? gmail[0] : gmail[0] + '@gmail.com', + user: gmail[0], password: gmail[1] + ) +}