Advertisement
Guest User

Untitled

a guest
Jul 14th, 2016
286
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Groovy 24.83 KB | None | 0 0
  1. // filebot -script "fn:amc" --output "X:/media" --action copy --conflict override --def subtitles=en music=y artwork=y "ut_dir=%D" "ut_file=%F" "ut_kind=%K" "ut_title=%N" "ut_label=%L" "ut_state=%S"
  2. // "C:/Program Files/FileBot/filebot.launcher.exe" -script "I:/Downloads/Links/amc.groovy" --output "I:/Downloads/Links" --log-file "I:/Downloads/Links/amc.log" --action hardlink --conflict override -non-strict --def music=y excludeList="I:/Downloads/Links/amc-input.txt" plex="127.0.0.1" "ut_label=%L" "ut_state=%S" "ut_title=%N" "ut_kind=%K" "ut_file=%F" "ut_dir=%D"
  3.  
  4. // log input parameters
  5. log.fine("Run script [${_args.script}] at [${now}]")
  6. _def.each{ n, v -> log.finer('Parameter: ' + [n, n =~ /pushover|pushbullet|mail|myepisodes/ ? '*****' : v].join(' = ')) }
  7. args.each{ log.finer("Argument: $it") }
  8.  
  9.  
  10. // initialize variables
  11. def input = []
  12. def failOnError = _args.conflict.equalsIgnoreCase('fail')
  13. def isTest = _args.action.equalsIgnoreCase('test')
  14.  
  15. // enable/disable features as specified via --def parameters
  16. def unsorted  = tryQuietly{ unsorted.toBoolean() }
  17. def music     = tryQuietly{ music.toBoolean() }
  18. def subtitles = tryQuietly{ subtitles.split(/\W+/) as List }
  19. def artwork   = tryQuietly{ artwork.toBoolean() && !isTest }
  20. def extras    = tryQuietly{ extras.toBoolean() }
  21. def clean     = tryQuietly{ clean.toBoolean() }
  22. def exec      = tryQuietly{ exec.toString() }
  23.  
  24. // array of xbmc/plex hosts
  25. def xbmc = tryQuietly{ xbmc.split(/[ ,|]+/) }
  26. def plex = tryQuietly{ plex.split(/[ ,|]+/)*.split(/:/).collect{ it.length >= 2 ? [host:it[0], token:it[1]] : [host:it[0]] } }
  27.  
  28. // extra options, myepisodes updates and email notifications
  29. def storeReport = tryQuietly{ storeReport.toBoolean() }
  30. def skipExtract = tryQuietly{ skipExtract.toBoolean() }
  31. def deleteAfterExtract = tryQuietly{ deleteAfterExtract.toBoolean() }
  32. def excludeList = tryQuietly{ (excludeList as File).isAbsolute() ? (excludeList as File) : new File(_args.output ?: '.', excludeList).getCanonicalFile() }
  33. def myepisodes = tryQuietly{ myepisodes.split(':', 2) }
  34. def gmail = tryQuietly{ gmail.split(':', 2) }
  35. def mail = tryQuietly{ mail.split(':', 3) }
  36. def pushover = tryQuietly{ pushover.split(':', 2) }
  37. def pushbullet = tryQuietly{ pushbullet.toString() }
  38. def reportError = tryQuietly{ reportError.toBoolean() }
  39.  
  40. // user-defined filters
  41. def label = tryQuietly{ ut_label } ?: null
  42. def ignore = tryQuietly{ ignore } ?: null
  43. def minFileSize = tryQuietly{ minFileSize.toLong() }; if (minFileSize == null) { minFileSize = 50 * 1000L * 1000L }
  44. def minLengthMS = tryQuietly{ minLengthMS.toLong() }; if (minLengthMS == null) { minLengthMS = 10 * 60 * 1000L }
  45.  
  46. // series/anime/movie format expressions
  47. def format = [
  48.     tvs:   any{ seriesFormat }{ '''TV Shows/{n}/{episode.special ? 'Special' : 'Season '+s.pad(2)}/{n} - {episode.special ? 'S00E'+special.pad(2) : s00e00} - {vf} - {t.replaceAll(/[`´‘’ʻ]/, /'/).replaceAll(/[!?.]+$/).replacePart(', Part $1')}{'.'+lang}''' },
  49.     anime: any{ animeFormat  }{ '''TV Shows/{primaryTitle}/{primaryTitle} - {sxe} - {vf} - {t.replaceAll(/[!?.]+$/).replaceAll(/[`´‘’ʻ]/, /'/).replacePart(', Part $1')}''' },
  50.     mov:   any{ movieFormat  }{ '''Movies/{n} ({y})/{n} ({y}) - {vf}{' CD'+pi}{'.'+lang}''' },
  51.     music: any{ musicFormat  }{ '''Music/{n}/{album+'/'}{pi.pad(2)+'. '}{artist} - {t}''' },
  52.     unsorted: any{ unsortedFormat }{ '''Unsorted/{file.structurePathTail}''' }
  53. ]
  54.  
  55.  
  56. // force movie/series/anime logic
  57. def forceMovie = { f ->
  58.     label =~ /^(?i:Movie|Couch.Potato)/ || f.dir.listPath().any{ it.name ==~ /(?i:Movies)/ }  || f.path =~ /(?<=tt)\\d{7}/
  59. }
  60.  
  61. def forceSeries = { f ->
  62.     label =~ /^(?i:TV|Kids.Shows)/ || f.dir.listPath().any{ it.name ==~ /(?i:TV.Shows)/ } || parseEpisodeNumber(f.path) || parseDate(f.path) || f.path =~ /(?i:tvs-|tvp-|EP[0-9]{2,3}|Season\D?[0-9]{1,2}\D|(19|20)\d{2}.S\d{2})/
  63. }
  64.  
  65. def forceAnime = { f ->
  66.     label =~ /^(?i:Anime)/ || f.dir.listPath().any{ it.name ==~ /(?i:Anime)/ } || (f.isVideo() && (f.name =~ /(?i:HorribleSubs)/ || f.name =~ "[\\(\\[]\\p{XDigit}{8}[\\]\\)]" || (getMediaInfo(file:f, format:'''{media.AudioLanguageList} {media.TextCodecList}''').tokenize().containsAll(['Japanese', 'ASS']) && (parseEpisodeNumber(f.name, false) != null || getMediaInfo(file:f, format:'{minutes}').toInteger() < 60))))
  67. }
  68.  
  69. def forceAudio = { f ->
  70.     label =~ /^(?i:audio|music|music.video)/ || (f.isAudio() && !f.isVideo())
  71. }
  72.  
  73. def forceIgnore = { f ->
  74.     label =~ /^(?i:games|ebook|other|ignore|seeding)/ || f.path.findMatch(ignore) != null
  75. }
  76.  
  77.  
  78.  
  79. // include artwork/nfo, pushover/pushbullet and ant utilities as required
  80. if (artwork || xbmc || plex) { include('lib/htpc') }
  81. if (pushover || pushbullet ) { include('lib/web') }
  82. if (gmail || mail) { include('lib/ant') }
  83.  
  84.  
  85.  
  86. // error reporting functions
  87. def sendEmailReport = { title, message, messagetype ->
  88.     if (gmail) {
  89.         sendGmail(
  90.             subject: title,
  91.             message: message,
  92.             messagemimetype: messagetype,
  93.             to: any{ mailto } { gmail[0] + '@gmail.com' }, // mail to self by default
  94.             user: gmail[0],
  95.             password: gmail[1]
  96.         )
  97.     }
  98.     if (mail) {
  99.         sendmail(
  100.             mailhost: mail[0],
  101.             mailport: mail[1],
  102.             from: mail[2],
  103.             to: mailto,
  104.             subject: title,
  105.             message: message,
  106.             messagemimetype: messagetype
  107.         )
  108.     }
  109. }
  110.  
  111. def fail = { message ->
  112.     if (reportError) {
  113.         sendEmailReport('[FileBot] Failure', message, 'text/plain')
  114.     }
  115.     die(message)
  116. }
  117.  
  118.  
  119.  
  120. // sanity checks
  121. args.findAll{ !it.exists() }.each{ fail("File not found: $it") }
  122.  
  123. // check user-defined pre-condition
  124. if (tryQuietly{ !(ut_state ==~ ut_state_allow) }) {
  125.     fail("Invalid state: ut_state = $ut_state (expected $ut_state_allow)")
  126. }
  127.  
  128. // check ut mode vs standalone mode
  129. if ((tryQuietly{ ut_dir } == '/') || (args.size() > 0 && (tryQuietly{ ut_dir }?.size() > 0 || tryQuietly{ ut_file }?.size() > 0)) || (args.size() == 0 && (tryQuietly{ ut_dir } == null && tryQuietly{ ut_file } == null))) {
  130.     fail("Invalid arguments: pass in either file arguments or ut_dir/ut_file parameters but not both")
  131. }
  132.  
  133.  
  134.  
  135. // define and load exclude list (e.g. to make sure files are only processed once)
  136. def excludePathSet = new FileSet()
  137. if (excludeList) {
  138.     if (excludeList.exists()) {
  139.         excludePathSet.feed(Files.lines(excludeList.toPath(), StandardCharsets.UTF_8))
  140.         log.finest "Using excludes: ${excludeList} (${excludePathSet.size()})"
  141.     } else if ((!excludeList.parentFile.isDirectory() && !excludeList.parentFile.mkdirs()) || (!excludeList.isFile() && !excludeList.createNewFile())) {
  142.         die("Failed to create excludeList: ${excludeList}")
  143.     }
  144. }
  145.  
  146.  
  147. // specify how to resolve input folders, e.g. grab files from all folders except disk folders and already processed folders (i.e. folders with movie/tvshow nfo files)
  148. def resolveInput(f) {
  149.     // ignore system and hidden folders
  150.     if (f.isHidden()) {
  151.         if (f.isDirectory()) log.finest "Ignore hidden: $f" // ignore all hidden files but only log hidden folders
  152.         return []
  153.     }
  154.  
  155.     // ignore already processed folders
  156.     if (f.isDirectory() && f.listFiles().toList().any{ it.name ==~ /movie.nfo|tvshow.nfo/ }) {
  157.         log.finest "Ignore processed folder: $f"
  158.         return []
  159.     }
  160.  
  161.     // resolve recursively
  162.     if (f.isDirectory() && !f.isDisk())
  163.         return f.listFiles().toList().findResults{ resolveInput(it) }
  164.     else
  165.         return f
  166. }
  167.  
  168. // collect input fileset as specified by the given --def parameters
  169. def roots = []
  170. if (args.empty) {
  171.     // assume we're called with utorrent parameters (account for older and newer versions of uTorrents)
  172.     if (ut_kind == 'single' || (ut_kind != 'multi' && ut_dir && ut_file)) {
  173.         roots += new File(ut_dir, ut_file) // single-file torrent
  174.     } else {
  175.         roots += new File(ut_dir) // multi-file torrent
  176.     }
  177. } else {
  178.     // assume we're called normally with arguments
  179.     roots += args
  180. }
  181.  
  182. // sanitize input
  183. roots = roots.findAll{ it?.exists() }.collect{ it.canonicalFile }.unique() // roots could be folders as well as files
  184.  
  185. // flatten nested file structure
  186. input = roots.flatten{ f -> resolveInput(f) }
  187.  
  188. // ignore archives that are on the exclude path list
  189. input = input.findAll{ f -> !excludePathSet.contains(f.path) }
  190.  
  191. // extract archives (zip, rar, etc) that contain at least one video file
  192. def extractedArchives = []
  193. def tempFiles = []
  194. input = input.flatten{ f ->
  195.     if (!skipExtract && (f.isArchive() || f.hasExtension('001'))) {
  196.         def extractDir = new File(f.dir, f.nameWithoutExtension)
  197.         def extractFiles = extract(file: f, output: new File(extractDir, f.dir.name), conflict: 'auto', filter: { it.isArchive() || it.isVideo() || it.isSubtitle() || (music && it.isAudio()) }, forceExtractAll: true) ?: []
  198.  
  199.         if (extractFiles.size() > 0) {
  200.             extractedArchives += f
  201.             tempFiles += extractDir
  202.             tempFiles += extractFiles
  203.         }
  204.         return extractFiles
  205.     }
  206.     return f
  207. }
  208.  
  209.  
  210. // ignore files that are on the exclude path list
  211. input = input.findAll{ f -> !excludePathSet.contains(f.path) }
  212.  
  213. // update exclude list with all input that will be processed during this run
  214. if (excludeList && !isTest) {
  215.     excludeList.withWriterAppend('UTF-8') { out ->
  216.         extractedArchives.path.each{ out.println(it) }
  217.         input.path.each{ out.println(it) }
  218.     }
  219. }
  220.  
  221.  
  222. // helper function to work with the structure relative path rather than the whole absolute path
  223. def relativeInputPath = { f ->
  224.     def r = roots.find{ r -> f.path.startsWith(r.path) && r.isDirectory() && f.isFile() }
  225.     if (r != null) {
  226.         return f.path.substring(r.path.length() + 1)
  227.     }
  228.     return f.name
  229. }
  230.  
  231.  
  232. // keep original input around so we can print excluded files later
  233. def originalInputSet = input as LinkedHashSet
  234.  
  235. // process only media files
  236. input = input.findAll{ f -> (f.isVideo() && !tryQuietly{ f.hasExtension('iso') && !f.isDisk() }) || f.isSubtitle() || (f.isDirectory() && f.isDisk()) || (music && f.isAudio()) }
  237.  
  238. // ignore clutter files
  239. input = input.findAll{ f -> !(relativeInputPath(f) =~ /(?<=\b|_)(?i:sample|trailer|extras|music.video|scrapbook|behind.the.scenes|extended.scenes|deleted.scenes|mini.series|s\d{2}c\d{2}|S\d+EXTRA|\d+xEXTRA|NCED|NCOP|(OP|ED)\d+|Formula.1.\d{4})(?=\b|_)/) }
  240.  
  241. // ignore video files that don't conform with the file-size and video-length limits
  242. input = input.findAll{ f -> !(f.isVideo() && ((minFileSize > 0 && f.length() < minFileSize) || (minLengthMS > 0 && tryQuietly{ getMediaInfo(file:f, format:'{duration}').toLong() < minLengthMS }))) }
  243.  
  244. // ignore subtitles files that are not stored in the same folder as the movie
  245. input = input.findAll{ f -> !(f.isSubtitle() && !input.findAll{ it.isVideo() }.any{ f.isDerived(it) }) }
  246.  
  247. // ensure that the final input set is sorted
  248. input = input.sort()
  249.  
  250. // print exclude and input sets for logging
  251. input.each{ f -> log.finer("Input: $f") }
  252. (originalInputSet - input).each{ f -> log.finest("Exclude: $f") }
  253.  
  254. // early abort if there is nothing to do
  255. if (input.size() == 0) die("No files selected for processing")
  256.  
  257.  
  258.  
  259. // group episodes/movies and rename according to XBMC standards
  260. def groups = input.groupBy{ f ->
  261.     // skip auto-detection if possible
  262.     if (forceIgnore(f))
  263.         return []
  264.     if (music && forceAudio(f)) // process audio only if music mode is enabled
  265.         return [music: f.dir.name]
  266.     if (forceMovie(f))
  267.         return [mov:   detectMovie(f, false)]
  268.     if (forceSeries(f))
  269.         return [tvs:   detectSeriesName(f, true, false) ?: detectSeriesName(input.findAll{ s -> f.dir == s.dir && s.isVideo() }, true, false)]
  270.     if (forceAnime(f))
  271.         return [anime: detectSeriesName(f, false, true) ?: detectSeriesName(input.findAll{ s -> f.dir == s.dir && s.isVideo() }, false, true)]
  272.    
  273.    
  274.     def tvs = detectSeriesName(f, true, false)
  275.     def mov = detectMovie(f, false)
  276.     log.fine("$f.name [series: $tvs, movie: $mov]")
  277.    
  278.     // DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
  279.     if (tvs && mov) {
  280.         def norm = { s -> s.ascii().normalizePunctuation().lower().space(' ') }
  281.         def dn = norm(guessMovieFolder(f)?.name ?: '')
  282.         def fn = norm(f.nameWithoutExtension)
  283.         def sn = norm(tvs)
  284.         def mn = norm(mov.name)
  285.         def my = mov.year as String
  286.        
  287.         /*
  288.         println '--- EPISODE FILTER (POS) ---'
  289.         println parseEpisodeNumber(fn, true) || parseDate(fn)
  290.         println ([dn, fn].find{ it =~ sn && matchMovie(it) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || stripReleaseInfo(fn.after(sn), false) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn) == null)
  291.         println (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn) == null)
  292.         println f.dir.listFiles{ it.isVideo() && (dn =~ sn || norm(it.name) =~ sn) && it.name =~ /\d{1,3}/}.findResults{ it.name.matchAll(/\d{1,3}/) as Set }.unique().size() >= 10
  293.         println '--- EPISODE FILTER (NEG) ---'
  294.         println (mn == fn)
  295.         println (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name.contains(my) && parseEpisodeNumber(it.name.after(my), false) == null })
  296.         println (mn =~ sn && [dn, fn].find{ it =~ /\b(19|20)\d{2}\b/ && parseEpisodeNumber(it.after(/\b(19|20)\d{2}\b/), false) == null })
  297.         println '--- MOVIE FILTER (POS) ---'
  298.         println (fn.contains(mn) && parseEpisodeNumber(fn.after(mn), false) == null)
  299.         println (mn.getSimilarity(fn) >= 0.8 || [dn, fn].find{ it.findAll( ~/\d{4}/ ).findAll{ y -> [mov.year-1, mov.year, mov.year+1].contains(y.toInteger()) }.size() > 0 } != null)
  300.         println ([dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && (it.getSimilarity(mn) > 0.2 + it.getSimilarity(sn)) } != null)
  301.         println (detectMovie(f, true) && [dn, fn].find{ it =~ /(19|20)\d{2}/ } != null)
  302.         */
  303.        
  304.         // S00E00 | 2012.07.21 | One Piece 217 | Firefly - Serenity | [Taken 1, Taken 2, Taken 3, Taken 4, ..., Taken 10]
  305.         if ((parseEpisodeNumber(fn, true) || parseDate(fn) || ([dn, fn].find{ it =~ sn && matchMovie(it) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || stripReleaseInfo(fn.after(sn), false) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn) == null) || (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn) == null) || f.dir.listFiles{ it.isVideo() && (dn =~ sn || norm(it.name) =~ sn) && it.name =~ /\d{1,3}/}.findResults{ it.name.matchAll(/\d{1,3}/) as Set }.unique().size() >= 10 || mov.year < 1900) && !( (mn == fn) || (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name.contains(my) && parseEpisodeNumber(it.name.after(my), false) == null }) || (mn =~ sn && [dn, fn].find{ it =~ /\b(19|20)\d{2}\b/ && parseEpisodeNumber(it.after(/\b(19|20)\d{2}\b/), false) == null }) ) ) {
  306.             log.fine("Exclude Movie: $mov")
  307.             mov = null
  308.         } else if ((fn.contains(mn) && parseEpisodeNumber(fn.after(mn), false) == null) || (mn.getSimilarity(fn) >= 0.8 || [dn, fn].find{ it.findAll( ~/\d{4}/ ).findAll{ y -> [mov.year-1, mov.year, mov.year+1].contains(y.toInteger()) }.size() > 0 } != null) || ([dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && (it.getSimilarity(mn) > 0.2 + it.getSimilarity(sn)) } != null) || (detectMovie(f, false) && [dn, fn].find{ it =~ /(19|20)\d{2}|(?i:CD)[1-9]/ } != null)) {
  309.             log.fine("Exclude Series: $tvs")
  310.             tvs = null
  311.         }
  312.     }
  313.    
  314.     // CHECK CONFLICT
  315.     if (((mov && tvs) || (!mov && !tvs))) {
  316.         if (failOnError) {
  317.             fail("Media detection failed")
  318.         } else {
  319.             log.fine("Unable to differentiate: [$f.name] => [$tvs] VS [$mov]")
  320.             return [tvs: null, mov: null, anime: null]
  321.         }
  322.     }
  323.    
  324.     return [tvs: tvs, mov: mov, anime: null]
  325. }
  326.  
  327. // group entries by unique tvs/mov descriptor
  328. groups = groups.groupBy{ group, files -> group.collectEntries{ type, query -> [type, query ? query.toString().ascii().normalizePunctuation().lower() : null] } }.collectEntries{ group, maps -> [group, maps.values().flatten()] }
  329.  
  330. // log movie/series/anime detection results
  331. groups.each{ group, files -> log.finest("Group: $group => ${files*.name}") }
  332.  
  333. // process each batch
  334. groups.each{ group, files ->
  335.     // fetch subtitles (but not for anime)
  336.     if (group.anime == null && subtitles != null && files.findAll{ it.isVideo() }.size() > 0) {
  337.         subtitles.each{ languageCode ->
  338.             def subtitleFiles = getMissingSubtitles(file:files, lang:languageCode, strict:true, output:'srt', encoding:'UTF-8', db: 'OpenSubtitles', format:'MATCH_VIDEO_ADD_LANGUAGE_TAG') ?: []
  339.             files += subtitleFiles
  340.             input += subtitleFiles // make sure subtitles are added to the exclude list and other post processing operations
  341.             tempFiles += subtitleFiles // if downloaded for temporarily extraced files delete later
  342.         }
  343.     }
  344.    
  345.     // EPISODE MODE
  346.     if ((group.tvs || group.anime) && !group.mov) {
  347.         // choose series / anime config
  348.         def config = group.tvs ? [name:group.tvs,   format:format.tvs,   db:'TheTVDB']
  349.                                : [name:group.anime, format:format.anime, db:'AniDB']
  350.         def dest = rename(file: files, format: config.format, db: config.db)
  351.         if (dest && artwork) {
  352.             dest.mapByFolder().each{ dir, fs ->
  353.                 def hasSeasonFolder = (config.format =~ /(?i)Season/)
  354.                 def sxe = fs.findResult{ eps -> parseEpisodeNumber(eps) }
  355.                 def seriesName = detectSeriesName(fs, true, false)
  356.                 def options = TheTVDB.search(seriesName, _args.locale)
  357.                 if (options.isEmpty()) {
  358.                     log.warning "TV Series not found: $config.name"
  359.                     return
  360.                 }
  361.                 def series = options.sortBySimilarity(seriesName, { s -> s.name }).get(0)
  362.                 log.fine "Fetching series artwork for [$series] to [$dir]"
  363.                 fetchSeriesArtworkAndNfo(hasSeasonFolder ? dir.dir : dir, dir, series, sxe && sxe.season > 0 ? sxe.season : 1, false, _args.locale)
  364.             }
  365.         }
  366.         if (dest == null && failOnError) {
  367.             fail("Failed to rename series: $config.name")
  368.         }
  369.     }
  370.    
  371.     // MOVIE MODE
  372.     else if (group.mov && !group.tvs && !group.anime) {
  373.         def dest = rename(file:files, format:format.mov, db:'TheMovieDB')
  374.         if (dest && artwork) {
  375.             dest.mapByFolder().each{ dir, fs ->
  376.                 def movieFile = fs.findAll{ it.isVideo() || it.isDisk() }.sort{ it.length() }.reverse().findResult{ it }
  377.                 if (movieFile != null) {
  378.                     def movie = detectMovie(movieFile, false)
  379.                     log.fine "Fetching movie artwork for [$movie] to [$dir]"
  380.                     fetchMovieArtworkAndNfo(dir, movie, movieFile, extras, false, _args.locale)
  381.                 }
  382.             }
  383.         }
  384.         if (dest == null && failOnError) {
  385.             fail("Failed to rename movie: $group.mov")
  386.         }
  387.     }
  388.    
  389.     // MUSIC MODE
  390.     else if (group.music) {
  391.         def dest = rename(file:files, format:format.music, db:'AcoustID')
  392.         if (dest == null && failOnError) {
  393.             fail("Failed to rename music: $group.music")
  394.         }
  395.     }
  396. }
  397.  
  398.  
  399. // ---------- POST PROCESSING ---------- //
  400.  
  401. // deal with remaining files that cannot be sorted automatically
  402. if (unsorted) {
  403.     def unsortedFiles = (input - getRenameLog().keySet())
  404.     if (unsortedFiles.size() > 0) {
  405.         log.info "Processing ${unsortedFiles.size()} unsorted files"
  406.         rename(map: unsortedFiles.collectEntries{ original ->
  407.             [original, new File(_args.output, getMediaInfo(file: original, format: format.unsorted))]
  408.         })
  409.     }
  410. }
  411.  
  412. // run program on newly processed files
  413. if (exec) {
  414.     getRenameLog().each{ from, to ->
  415.         def command = getMediaInfo(format: exec, file: to)
  416.         log.finest("Execute: $command")
  417.         execute(command)
  418.     }
  419. }
  420.  
  421.  
  422. // ---------- REPORTING ---------- //
  423.  
  424.  
  425. if (getRenameLog().size() > 0) {
  426.    
  427.     // messages used for xbmc / plex / pushover notifications
  428.     def getNotificationTitle = { "FileBot finished processing ${getRenameLog().values().findAll{ !it.isSubtitle() }.size()} files" }.memoize()
  429.     def getNotificationMessage = { prefix = '• ', postfix = '\n' -> tryQuietly{ ut_title } ?: (input.any{ !it.isSubtitle() } ? input.findAll{ !it.isSubtitle() } : input).collect{ relativeInputPath(it) as File }*.getRoot()*.getNameWithoutExtension().unique().sort{ it.toLowerCase() }.collect{ prefix + it }.join(postfix).trim() }.memoize()
  430.    
  431.     // make XMBC scan for new content and display notification message
  432.     if (xbmc) {
  433.         xbmc.each{ host ->
  434.             log.info "Notify XBMC: $host"
  435.             tryLogCatch{
  436.                 showNotification(host, 9090, getNotificationTitle(), getNotificationMessage(), 'http://app.filebot.net/icon.png')
  437.                 scanVideoLibrary(host, 9090)
  438.             }
  439.         }
  440.     }
  441.    
  442.     // make Plex scan for new content
  443.     if (plex) {
  444.         plex.each{ instance ->
  445.             log.info "Notify Plex: ${instance.host}"
  446.             tryLogCatch {
  447.                 refreshPlexLibrary(instance.host, 32400, instance.token)
  448.             }
  449.         }
  450.     }
  451.    
  452.     // mark episodes as 'acquired'
  453.     if (myepisodes) {
  454.         log.info 'Update MyEpisodes'
  455.         tryLogCatch {
  456.             executeScript('update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
  457.         }
  458.     }
  459.    
  460.     if (pushover) {
  461.         log.info 'Sending Pushover notification'
  462.         tryLogCatch {
  463.             Pushover(pushover[0], pushover.length == 1 ? 'wcckDz3oygHSU2SdIptvnHxJ92SQKK' : pushover[1]).send(getNotificationTitle(), getNotificationMessage())
  464.         }
  465.     }
  466.    
  467.     // messages used for email / pushbullet reports
  468.     def getReportSubject = { getNotificationMessage('', ', ') }
  469.     def getReportTitle = { '[FileBot] ' + getReportSubject() }
  470.     def getReportMessage = {
  471.         def renameLog = getRenameLog()
  472.         '''<!DOCTYPE html>\n''' + XML {
  473.             html {
  474.                 head {
  475.                     meta(charset:'UTF-8')
  476.                     style('''
  477.                         p{font-family:Arial,Helvetica,sans-serif}
  478.                         p b{color:#07a}
  479.                         hr{border-style:dashed;border-width:1px 0 0 0;border-color:lightgray}
  480.                         small{color:#d3d3d3;font-size:xx-small;font-weight:normal;font-family:Arial,Helvetica,sans-serif}
  481.                         table a:link{color:#666;font-weight:bold;text-decoration:none}
  482.                         table a:visited{color:#999;font-weight:bold;text-decoration:none}
  483.                         table a:active,table a:hover{color:#bd5a35;text-decoration:underline}
  484.                         table{font-family:Arial,Helvetica,sans-serif;color:#666;background:#eaebec;margin:15px;border:#ccc 1px solid;border-radius:3px;box-shadow:0 1px 2px #d1d1d1}
  485.                         table th{padding:15px;border-top:1px solid #fafafa;border-bottom:1px solid #e0e0e0;background:#ededed}
  486.                         table th{text-align:center;padding-left:20px}
  487.                         table tr:first-child th:first-child{border-top-left-radius:3px}
  488.                         table tr:first-child th:last-child{border-top-right-radius:3px}
  489.                         table tr{text-align:left;padding-left:20px}
  490.                         table td:first-child{text-align:left;padding-left:20px;border-left:0}
  491.                         table td{padding:15px;border-top:1px solid #fff;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;background:#fafafa;white-space:nowrap}
  492.                         table tr.even td{background:#f6f6f6}
  493.                         table tr:last-child td{border-bottom:0}
  494.                         table tr:last-child td:first-child{border-bottom-left-radius:3px}
  495.                         table tr:last-child td:last-child{border-bottom-right-radius:3px}
  496.                         table tr:hover td{background:#f2f2f2}
  497.                     ''')
  498.                     title(getReportTitle())
  499.                 }
  500.                 body {
  501.                     p {
  502.                         mkp.yield("FileBot finished processing ")
  503.                         b(getReportSubject())
  504.                         mkp.yield(" (${renameLog.size()} files).")
  505.                     }
  506.                     hr(); table {
  507.                         tr { th('Original Name'); th('New Name'); th('New Location') }
  508.                         renameLog.each{ from, to ->
  509.                             tr { [from.name, to.name, to.parent].each{ cell -> td(cell) } }
  510.                         }
  511.                     }
  512.                     hr(); small("// Generated by ${Settings.getApplicationIdentifier()} on ${InetAddress.localHost.hostName} at ${now.dateTimeString}")
  513.                 }
  514.             }
  515.         }
  516.     }
  517.    
  518.     // store processing report
  519.     if (storeReport) {
  520.         def reportFolder = new File(Settings.getApplicationFolder(), 'reports').getCanonicalFile()
  521.         def reportFile = getReportMessage().saveAs(new File(reportFolder, "AMC ${now.format('''[yyyy-MM-dd HH'h'mm'm']''')} ${getReportSubject().take(50).trim()}.html".validateFileName()))
  522.         log.finest("Saving report as ${reportFile}")
  523.     }
  524.    
  525.     // send pushbullet report
  526.     if (pushbullet) {
  527.         log.info 'Sending PushBullet report'
  528.         tryLogCatch {
  529.             PushBullet(pushbullet).sendFile(getNotificationTitle(), getReportMessage(), 'text/html', getNotificationMessage(), tryQuietly{ mailto })
  530.         }
  531.     }
  532.    
  533.     // send email report
  534.     if (gmail || mail) {
  535.         tryLogCatch {
  536.             sendEmailReport(getReportTitle(), getReportMessage(), 'text/html')
  537.         }
  538.     }
  539. }
  540.  
  541.  
  542. // ---------- CLEAN UP ---------- //
  543.  
  544.  
  545. // clean up temporary files that may be left behind after extraction
  546. if (deleteAfterExtract) {
  547.     extractedArchives.each{ a ->
  548.         log.finest("Delete archive $a")
  549.         a.delete()
  550.         a.dir.listFiles().toList().findAll{ v -> v.name.startsWith(a.nameWithoutExtension) && v.extension ==~ /r\d+/ }.each{ v ->
  551.             log.finest("Delete archive volume $v")
  552.             v.delete()
  553.         }
  554.     }
  555. }
  556.  
  557. // clean empty folders, clutter files, etc after move
  558. if (clean) {
  559.     if (['COPY', 'HARDLINK'].find{ it.equalsIgnoreCase(_args.action) } && tempFiles.size() > 0) {
  560.         log.info 'Clean temporary extracted files'
  561.         // delete extracted files
  562.         tempFiles.findAll{ it.isFile() }.sort().each{
  563.             log.finest "Delete $it"
  564.             it.delete()
  565.         }
  566.         // delete remaining empty folders
  567.         tempFiles.findAll{ it.isDirectory() }.sort().reverse().each{
  568.             log.finest "Delete $it"
  569.             if (it.getFiles().isEmpty()) it.deleteDir()
  570.         }
  571.     }
  572.    
  573.     // deleting remaining files only makes sense after moving files
  574.     if ('MOVE'.equalsIgnoreCase(_args.action)) {
  575.         def cleanerInput = !args.empty ? args : ut_kind == 'multi' && ut_dir ? [ut_dir as File] : []
  576.         cleanerInput = cleanerInput.findAll{ f -> f.exists() }
  577.         if (cleanerInput.size() > 0) {
  578.             log.info 'Clean clutter files and empty folders'
  579.             executeScript('cleaner', args.empty ? [root:true, ignore: ignore] : [root:false, ignore: ignore], cleanerInput)
  580.         }
  581.     }
  582. }
  583.  
  584.  
  585. if (getRenameLog().size() == 0) fail("Finished without processing any files")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement