Advertisement
filebot

AMC with Custom Rename Function and Delay

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