squadjot

AMC

Dec 31st, 2013
284
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Groovy 15.31 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. def input = []
  3. def failOnError = _args.conflict == 'fail'
  4.  
  5. // print input parameters
  6. _args.bindings?.each{ _log.fine("Parameter: $it.key = $it.value") }
  7. args.each{ _log.fine("Argument: $it") }
  8. args.findAll{ !it.exists() }.each{ throw new Exception("File not found: $it") }
  9.  
  10. // check user-defined pre-condition
  11. if (tryQuietly{ !(ut_state ==~ ut_state_allow) }) {
  12.     throw new Exception("Invalid state: ut_state = $ut_state (expected $ut_state_allow)")
  13. }
  14.  
  15. // check ut mode vs standalone mode
  16. 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))) {
  17.     throw new Exception("Conflicting arguments: pass in either file arguments or ut_dir/ut_file parameters but not both")
  18. }
  19.  
  20. // enable/disable features as specified via --def parameters
  21. def music     = tryQuietly{ music.toBoolean() }
  22. def subtitles = tryQuietly{ subtitles.toBoolean() ? ['en'] : subtitles.split(/[ ,|]+/).findAll{ it.length() >= 2 } }
  23. def artwork   = tryQuietly{ artwork.toBoolean() }
  24. def backdrops = tryQuietly{ backdrops.toBoolean() }
  25. def clean     = tryQuietly{ clean.toBoolean() }
  26. def exec      = tryQuietly{ exec.toString() }
  27.  
  28. // array of xbmc/plex hosts
  29. def xbmc = tryQuietly{ xbmc.split(/[ ,|]+/) }
  30. def plex = tryQuietly{ plex.split(/[ ,|]+/) }
  31.  
  32. // extra options, myepisodes updates and email notifications
  33. def deleteAfterExtract = tryQuietly{ deleteAfterExtract.toBoolean() }
  34. def excludeList = tryQuietly{ new File(_args.output, excludeList) }
  35. def myepisodes = tryQuietly{ myepisodes.split(':', 2) }
  36. def gmail = tryQuietly{ gmail.split(':', 2) }
  37. def pushover = tryQuietly{ pushover.toString() }
  38.  
  39. // user-defined filters
  40. def minFileSize = tryQuietly{ minFileSize.toLong() }; if (minFileSize == null) { minFileSize = 0 };
  41.  
  42. // series/anime/movie format expressions
  43. def format = [
  44.     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"}''',
  45.     anime: tryQuietly{ animeFormat  } ?: '''Anime/{n}/{n} - {sxe} - {t.replaceAll(/[!?.]+$/).replaceAll(/[`´‘’ʻ]/, "'").replacePart(', Part $1')}''',
  46.     mov:   tryQuietly{ movieFormat  } ?: '''Movies/{n} ({y})/{n} ({y}){" CD$pi"}{".$lang"}''',
  47.     music: tryQuietly{ musicFormat  } ?: '''Music/{n}/{album+'/'}{pi.pad(2)+'. '}{artist} - {t}'''
  48. ]
  49.  
  50.  
  51. // force movie/series/anime logic
  52. def forceMovie(f) {
  53.     tryQuietly{ ut_label } =~ /^(?i:Movie|Couch.Potato)/ || f.dir.path =~ /\b(?i:Movies)\b/ || f.path =~ /(?<=tt)\\d{7}/ || tryQuietly{ f.metadata?.object?.class.name =~ /Movie/ }
  54. }
  55.  
  56. def forceSeries(f) {
  57.     tryQuietly{ ut_label } =~ /^(?i:TV|Kids.Shows)/ || f.dir.path =~ /\b(?i:TV.Shows)\b/ || parseEpisodeNumber(f.path) || parseDate(f.path) || f.path =~ /(?i:Season)\D?[0-9]{1,2}\D/ || tryQuietly{ f.metadata?.object?.class.name =~ /Episode/ }
  58. }
  59.  
  60. def forceAnime(f) {
  61.     tryQuietly{ ut_label } =~ /^(?i:Anime)/ || f.dir.path =~ /\b(?i:Anime)\b/ || (f.isVideo() && (f.name =~ "[\\(\\[]\\p{XDigit}{8}[\\]\\)]" || getMediaInfo(file:f, format:'''{media.AudioLanguageList} {media.TextCodecList}''').tokenize().containsAll(['Japanese', 'ASS'])))
  62. }
  63.  
  64. def forceIgnore(f) {
  65.     tryQuietly{ ut_label } =~ /^(?i:ebook|other|ignore)/ || f.path =~ tryQuietly{ ignore }
  66. }
  67.  
  68.  
  69. // specify how to resolve input folders, e.g. grab files from all folders except disk folders
  70. def resolveInput(f) {
  71.     if (f.isDirectory() && !f.isDisk())
  72.         return f.listFiles().toList().findResults{ resolveInput(it) }
  73.     else
  74.         return f
  75. }
  76.  
  77. // collect input fileset as specified by the given --def parameters
  78. if (args.empty) {
  79.     // assume we're called with utorrent parameters (account for older and newer versions of uTorrents)
  80.     if (ut_kind == 'single' || (ut_kind != 'multi' && ut_dir && ut_file)) {
  81.         input += new File(ut_dir, ut_file) // single-file torrent
  82.     } else {
  83.         input += resolveInput(ut_dir as File) // multi-file torrent
  84.     }
  85. } else {
  86.     // assume we're called normally with arguments
  87.     input += args.findResults{ resolveInput(it) }
  88. }
  89.  
  90.  
  91. // flatten nested file structure
  92. input = input.flatten()
  93.  
  94. // extract archives (zip, rar, etc) that contain at least one video file
  95. def extractedArchives = []
  96. def tempFiles = []
  97. input = input.flatten{ f ->
  98.     if (f.isArchive() || f.hasExtension('001')) {
  99.         def extractDir = new File(f.dir, f.nameWithoutExtension)
  100.         def extractFiles = extract(file: f, output: new File(extractDir, f.dir.name), conflict: 'skip', filter: { it.isArchive() || it.isVideo() || it.isSubtitle() || (music && it.isAudio()) }, forceExtractAll: true) ?: []
  101.        
  102.         if (extractFiles.size() > 0) {
  103.             extractedArchives += f
  104.             tempFiles += extractDir
  105.             tempFiles += extractFiles
  106.         }
  107.         return extractFiles
  108.     }
  109.     return f
  110. }
  111.  
  112. // sanitize input
  113. input = input.findAll{ it?.exists() }.collect{ it.canonicalFile }.unique()
  114.  
  115. // process only media files
  116. input = input.findAll{ f -> (f.isVideo() && !tryQuietly{ f.hasExtension('iso') && !f.isDisk() }) || f.isSubtitle() || (f.isDirectory() && f.isDisk()) || (music && f.isAudio()) }
  117.  
  118. // ignore clutter files
  119. input = input.findAll{ f -> !(f.path =~ /\b(?i:sample|trailer|extras|music.video|scrapbook|behind.the.scenes|extended.scenes|deleted.scenes|s\d{2}c\d{2}|mini.series)\b/ || (f.isFile() && f.length() < minFileSize)) }
  120.  
  121. // check and update exclude list (e.g. to make sure files are only processed once)
  122. if (excludeList) {
  123.     // check excludes from previous runs
  124.     def excludePathSet = excludeList.exists() ? excludeList.text.split('\n') as HashSet : []
  125.     input = input.findAll{ f -> !excludePathSet.contains(f.path) }
  126.    
  127.     // update excludes with input of this run
  128.     excludePathSet += input
  129.     excludePathSet.join('\n').saveAs(excludeList)
  130. }
  131.  
  132. // print input fileset
  133. input.each{ f -> _log.finest("Input: $f") }
  134.  
  135. // artwork/nfo utility
  136. if (artwork || xbmc || plex) { include('fn:lib/htpc') }
  137.  
  138. // group episodes/movies and rename according to XBMC standards
  139. def groups = input.groupBy{ f ->
  140.     // skip auto-detection if possible
  141.     if (forceIgnore(f))
  142.         return []
  143.     if (f.isAudio() && !f.isVideo()) // PROCESS MUSIC FOLDER BY FOLDER
  144.         return [music: f.dir.name]
  145.     if (forceMovie(f))
  146.         return [mov:   detectMovie(f, false)]
  147.     if (forceSeries(f))
  148.         return [tvs:   detectSeriesName(f) ?: detectSeriesName(f.dir.listFiles{ it.isVideo() })]
  149.     if (forceAnime(f))
  150.         return [anime: detectSeriesName(f) ?: detectSeriesName(f.dir.listFiles{ it.isVideo() })]
  151.    
  152.    
  153.     def tvs = detectSeriesName(f)
  154.     def mov = detectMovie(f, false)
  155.     _log.fine("$f.name [series: $tvs, movie: $mov]")
  156.    
  157.     // DECIDE EPISODE VS MOVIE (IF NOT CLEAR)
  158.     if (tvs && mov) {
  159.         def norm = { s -> s.ascii().normalizePunctuation().lower().space(' ') }
  160.         def dn = norm(guessMovieFolder(f)?.name ?: '')
  161.         def fn = norm(f.nameWithoutExtension)
  162.         def sn = norm(tvs)
  163.         def mn = norm(mov.name)
  164.        
  165.         /**
  166.         println '--- EPISODE FILTER (POS) ---'
  167.         println parseEpisodeNumber(fn, true) || parseDate(fn)
  168.         println ([dn, fn].find{ it =~ sn && matchMovie(it, true) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || fn.after(sn) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn, true) == null)
  169.         println (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn, true) == null)
  170.         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
  171.         println '--- EPISODE FILTER (NEG) ---'
  172.         println (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name =~ mov.year })
  173.         println (mn =~ sn && [dn, fn].find{ it =~ /(19|20)\d{2}/ })
  174.         **/
  175.        
  176.         // S00E00 | 2012.07.21 | One Piece 217 | Firefly - Serenity | [Taken 1, Taken 2, Taken 3, Taken 4, ..., Taken 10]
  177.         if ((parseEpisodeNumber(fn, true) || parseDate(fn) || ([dn, fn].find{ it =~ sn && matchMovie(it, true) == null } && (parseEpisodeNumber(stripReleaseInfo(fn.after(sn), false), false) || fn.after(sn) =~ /\D\d{1,2}\D{1,3}\d{1,2}\D/) && matchMovie(fn, true) == null) || (fn.after(sn) ==~ /.{0,3} - .+/ && matchMovie(fn, true) == 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) && !( (mov.year >= 1950 && f.listPath().reverse().take(3).find{ it.name =~ mov.year }) || (mn =~ sn && [dn, fn].find{ it =~ /(19|20)\d{2}/ }) ) ) {
  178.             _log.fine("Exclude Movie: $mov")
  179.             mov = null
  180.         } else if (similarity(mn, fn) >= 0.8 || [dn, fn].find{ it =~ /\b/+mov.year+/\b/ } || [dn, fn].find{ it =~ mn && !(it.after(mn) =~ /\b\d{1,3}\b/) && !(it.before(mn).contains(sn)) } || (detectMovie(f, true) && [dn, fn].find{ it =~ /(19|20)\d{2}/ })) {
  181.             _log.fine("Exclude Series: $tvs")
  182.             tvs = null
  183.         }
  184.     }
  185.    
  186.     // CHECK CONFLICT
  187.     if (((mov && tvs) || (!mov && !tvs))) {
  188.         if (failOnError) {
  189.             throw new Exception("Media detection failed")
  190.         } else {
  191.             _log.fine("Unable to differentiate: [$f.name] => [$tvs] VS [$mov]")
  192.             return [tvs: null, mov: null, anime: null]
  193.         }
  194.     }
  195.    
  196.     return [tvs: tvs, mov: mov, anime: null]
  197. }
  198.  
  199. // log movie/series/anime detection results
  200. groups.each{ group, files -> _log.finest("Group: $group => ${files*.name}") }
  201.  
  202. // process each batch
  203. groups.each{ group, files ->
  204.     // fetch subtitles (but not for anime)
  205.     if (subtitles && !group.anime && files.findAll{ it.isVideo() }.size() > 0) {
  206.         subtitles.each{ languageCode ->
  207.             def subtitleFiles = getMissingSubtitles(file:files, output:'srt', encoding:'UTF-8', lang:languageCode, strict:true) ?: []
  208.             files += subtitleFiles
  209.             tempFiles += subtitleFiles // if downloaded for temporarily extraced files delete later
  210.         }
  211.     }
  212.    
  213.     // EPISODE MODE
  214.     if ((group.tvs || group.anime) && !group.mov) {
  215.         // choose series / anime config
  216.         def config = group.tvs ? [name:group.tvs,   format:format.tvs,   db:'TheTVDB', seasonFolder:true ]
  217.                                : [name:group.anime, format:format.anime, db:'AniDB',   seasonFolder:false]
  218.         def dest = rename(file: files, format: config.format, db: config.db)
  219.         if (dest && artwork) {
  220.             dest.mapByFolder().each{ dir, fs ->
  221.                 _log.finest "Fetching artwork for $dir from TheTVDB"
  222.                 def sxe = fs.findResult{ eps -> parseEpisodeNumber(eps) }
  223.                 def options = TheTVDB.search(detectSeriesName(fs), _args.locale)
  224.                 if (options.isEmpty()) {
  225.                     _log.warning "TV Series not found: $config.name"
  226.                     return
  227.                 }
  228.                 options = options.sortBySimilarity(config.name, { s -> s.name })
  229.                 fetchSeriesArtworkAndNfo(config.seasonFolder ? dir.dir : dir, dir, options[0], sxe && sxe.season > 0 ? sxe.season : 1)
  230.             }
  231.         }
  232.         if (dest == null && failOnError) {
  233.             throw new Exception("Failed to rename series: $config.name")
  234.         }
  235.     }
  236.    
  237.     // MOVIE MODE
  238.     else if (group.mov && !group.tvs && !group.anime) {
  239.         def dest = rename(file:files, format:format.mov, db:'TheMovieDB')
  240.         if (dest && artwork) {
  241.             dest.mapByFolder().each{ dir, fs ->
  242.                 _log.finest "Fetching artwork for $dir from TheMovieDB"
  243.                 def movieFile = fs.findAll{ it.isVideo() }.sort{ it.length() }.reverse().findResult{ it }
  244.                 fetchMovieArtworkAndNfo(dir, detectMovie(movieFile), movieFile, backdrops)
  245.             }
  246.         }
  247.         if (dest == null && failOnError) {
  248.             throw new Exception("Failed to rename movie: $group.mov")
  249.         }
  250.     }
  251.    
  252.     // MUSIC MODE
  253.     else if (group.music) {
  254.         def dest = rename(file:files, format:format.music, db:'AcoustID')
  255.         if (dest == null && failOnError) {
  256.             throw new Exception("Failed to rename music: $group.music")
  257.         }
  258.     }
  259. }
  260.  
  261. // skip notifications if nothing was renamed anyway
  262. if (getRenameLog().isEmpty()) {
  263.     return
  264. }
  265.  
  266. // run program on newly processed files
  267. if (exec) {
  268.     getRenameLog().each{ from, to ->
  269.         def command = getMediaInfo(format: exec, file: to)
  270.         _log.finest("Execute: $command")
  271.         execute(command)
  272.     }
  273. }
  274.  
  275. // make XMBC scan for new content and display notification message
  276. if (xbmc) {
  277.     xbmc.each{ host ->
  278.         _log.info "Notify XBMC: $host"
  279.         _guarded{
  280.             showNotification(host, 9090, 'FileBot', "Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files).", 'http://www.filebot.net/images/icon.png')
  281.             scanVideoLibrary(host, 9090)
  282.         }
  283.     }
  284. }
  285.  
  286. // make Plex scan for new content
  287. if (plex) {
  288.     plex.each{
  289.         _log.info "Notify Plex: $it"
  290.         refreshPlexLibrary(it)
  291.     }
  292. }
  293.  
  294. // mark episodes as 'acquired'
  295. if (myepisodes) {
  296.     _log.info 'Update MyEpisodes'
  297.     executeScript('fn:update-mes', [login:myepisodes.join(':'), addshows:true], getRenameLog().values())
  298. }
  299.  
  300. if (pushover) {
  301.     // include webservice utility
  302.     include('fn:lib/ws')
  303.    
  304.     _log.info 'Sending Pushover notification'
  305.     Pushover(pushover).send("Finished processing ${tryQuietly { ut_title } ?: input*.dir.name.unique()} (${getRenameLog().size()} files).")
  306. }
  307.  
  308. // send status email
  309. if (gmail) {
  310.     // ant/mail utility
  311.     include('fn:lib/ant')
  312.    
  313.     // send html mail
  314.     def renameLog = getRenameLog()
  315.     def emailTitle = tryQuietly { ut_title } ?: input*.dir.name.unique()
  316.    
  317.     sendGmail(
  318.         subject: "[FileBot] ${emailTitle}",
  319.         message: XML {
  320.             html {
  321.                 body {
  322.                     p("FileBot finished processing ${emailTitle} (${renameLog.size()} files).");
  323.                     hr(); table {
  324.                         th("Parameter"); th("Value")
  325.                         _args.bindings.findAll{ param -> param.key =~ /^ut_/ }.each{ param ->
  326.                             tr { [param.key, param.value].each{ td(it)} }
  327.                         }
  328.                     }
  329.                     hr(); table {
  330.                         th("Original Name"); th("New Name"); th("New Location")
  331.                         renameLog.each{ from, to ->
  332.                             tr { [from.name, to.name, to.parent].each{ cell -> td{ nobr{ code(cell) } } } }
  333.                         }
  334.                     }
  335.                     hr(); small("// Generated by ${net.sourceforge.filebot.Settings.applicationIdentifier} on ${new Date().dateString} at ${new Date().timeString}")
  336.                 }
  337.             }
  338.         },
  339.         messagemimetype: 'text/html',
  340.         to: tryQuietly{ mailto } ?: gmail[0] + '@gmail.com', // mail to self by default
  341.         user: gmail[0], password: gmail[1]
  342.     )
  343. }
  344.  
  345. if (deleteAfterExtract) {
  346.     extractedArchives.each{ a ->
  347.         _log.finest("Delete archive $a")
  348.         a.delete()
  349.         a.dir.listFiles().toList().findAll{ v -> v.name.startsWith(a.nameWithoutExtension) && v.extension ==~ /r\d+/ }.each{ v ->
  350.             _log.finest("Delete archive volume $v")
  351.             v.delete()
  352.         }
  353.     }
  354. }
  355.  
  356. // clean empty folders, clutter files, etc after move
  357. if (clean) {
  358.     if (['COPY', 'HARDLINK'].find{ it.equalsIgnoreCase(_args.action) } && tempFiles.size() > 0) {
  359.         _log.info 'Clean temporary extracted files'
  360.         // delete extracted files
  361.         tempFiles.findAll{ it.isFile() }.sort().each{
  362.             _log.finest "Delete $it"
  363.             it.delete()
  364.         }
  365.         // delete remaining empty folders
  366.         tempFiles.findAll{ it.isDirectory() }.sort().reverse().each{
  367.             _log.finest "Delete $it"
  368.             if (it.getFiles().isEmpty()) it.deleteDir()
  369.         }
  370.     }
  371.    
  372.     // deleting remaining files only makes sense after moving files
  373.     if ('MOVE'.equalsIgnoreCase(_args.action)) {
  374.         def cleanerInput = !args.empty ? args : ut_kind == 'multi' && ut_dir ? [ut_dir as File] : []
  375.         cleanerInput = cleanerInput.findAll{ f -> f.exists() }
  376.         if (cleanerInput.size() > 0) {
  377.             _log.info 'Clean clutter files and empty folders'
  378.             executeScript('fn:cleaner', args.empty ? [root:true] : [root:false], cleanerInput)
  379.         }
  380.     }
  381. }
Advertisement
Add Comment
Please, Sign In to add comment